From d4b9b19e09fc0600b61ff60beb1bed36d7dd72c6 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 01/12] test(decode-js): use one global test timeout The heaviest jsconfuser and VM cases decode whole real-encoder samples, and v8 coverage and full parallel load multiply their time. One global limit replaces the per-test override, so a slow host or a coverage run is not a red build, while a genuine hang still fails. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- test/visitor/obfuscator/string-array.test.js | 2 +- vitest.config.js | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/test/visitor/obfuscator/string-array.test.js b/test/visitor/obfuscator/string-array.test.js index 7dc1a6cf..5c159b1d 100644 --- a/test/visitor/obfuscator/string-array.test.js +++ b/test/visitor/obfuscator/string-array.test.js @@ -205,7 +205,7 @@ describe('unreadable — the refusal, kept narrow', () => { timeout: 2000, }) expect(res.notes.some((n) => n.includes('did not evaluate'))).toBe(true) - }, 20000) + }) // An alias assigned without a declaration, so it is a global with no binding and its call sites // cannot be enumerated. Deleting the machinery here breaks the program rather than leaving diff --git a/vitest.config.js b/vitest.config.js index 9464e695..52cdc5d2 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -16,13 +16,11 @@ export default defineConfig({ "reportOnFailure": true, }, "passWithNoTests": true, - // The jsconfuser fixtures run the whole decode pipeline over real encoder - // output, and the largest are >100KB of obfuscated source - one full decode - // per test, with v8 coverage instrumenting every visitor it walks. The - // heaviest sits near 2s on a fast dev machine, which fits vitest's 5s - // default with too little margin for a shared CI runner: it has already - // timed out there while passing locally. Raised so a slow host is not a red - // build; a genuine hang still fails, just later. - "testTimeout": 30000, + // The heaviest jsconfuser and VM cases decode whole real-encoder samples, + // and their time grows several-fold under v8 coverage and full parallel + // load. One generous global limit replaces per-test overrides, so a slow + // host or a coverage run is not a red build; a genuine hang still fails, + // just later. + "testTimeout": 180000, }, }) From dc2ed02bf12b18dbf571997c39cef66d9223a102 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 02/12] test(decode-js): add a light test run The full suite runs the VM integration and qualification tests, which dominate its time. test:light runs the rest for quick checks during an edit; the full run stays the gate before a commit. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- package.json | 1 + vitest.light.config.js | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 vitest.light.config.js diff --git a/package.json b/package.json index 05d5edca..779a911e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "deso": "node src/main.js -t sojson", "desov7": "node src/main.js -t sojsonv7", "test": "vitest --config vitest.config.js", + "test:light": "vitest --config vitest.light.config.js", "lint": "eslint --fix src" }, "dependencies": { diff --git a/vitest.light.config.js b/vitest.light.config.js new file mode 100644 index 00000000..d41e157e --- /dev/null +++ b/vitest.light.config.js @@ -0,0 +1,26 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import base from './vitest.config.js' + +// The light run: the default suite minus the files that dominate its time. +// Each listed file is still part of `npm test`, which stays the full suite; run +// that before a commit. A file joins this list for its cost, never because it +// is failing. +export const heavy = [ + // Real-encoder samples decoded under stacked options, with native behavior. + 'test/vm/jsconfuser-vm/class-runtime-combinations.test.js', + 'test/vm/jsconfuser-vm/encoded-bytecode-combinations.test.js', + 'test/vm/jsconfuser-vm/normalize-alias-combinations.test.js', + 'test/vm/jsconfuser-vm/normalize-anti-routing-combinations.test.js', + 'test/vm/jsconfuser-vm/normalize-anti-specialized-combinations.test.js', + 'test/vm/jsconfuser-vm/normalize-macro-combinations.test.js', + 'test/vm/jsconfuser-vm/runtime-binding-identities.test.js', + // Large parameterized populations of retained cells. + 'test/vm/jsconfuser-vm/class-runtime-contract.test.js', + 'test/vm/jsconfuser-vm/minified-class-runtime.test.js', + 'test/vm/jsconfuser-vm/randomized-class.test.js', + 'test/vm/jsconfuser-vm/randomized-minified-class.test.js', + // Native replay of every retained record's observations. + 'test/vm/jsconfuser-vm/qualification-replay.test.js', +] + +export default mergeConfig(base, defineConfig({ test: { exclude: heavy } })) From db88e3cad402464994287ed8a0eae109ac3a12dc Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 03/12] chore(decode-js): retire duplicate plugin documentation docs/jsconfuser.md restated what the decode-nexus skill docs now own, so two copies could drift; the README points at the skill docs instead. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- README.md | 3 ++- docs/jsconfuser.md | 61 ---------------------------------------------- 2 files changed, 2 insertions(+), 62 deletions(-) delete mode 100644 docs/jsconfuser.md diff --git a/README.md b/README.md index d1185027..2ff49c37 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ A sample whose range overlaps that gap is reported as unknown rather than treate ### `jsconfuser` Covers JS-Confuser 2.x up to and including the `high` preset. Which transforms are -reversed, which are not, and why: [docs/jsconfuser.md](docs/jsconfuser.md). +reversed, which are not, and why are documented in the +[decode-nexus jsconfuser plugin reference](https://github.com/echo094/decode-nexus/blob/main/skills/decode-js/plugins/jsconfuser.md). ## Usage diff --git a/docs/jsconfuser.md b/docs/jsconfuser.md deleted file mode 100644 index 114a903b..00000000 --- a/docs/jsconfuser.md +++ /dev/null @@ -1,61 +0,0 @@ -# Decoding JS-Confuser output - -`-t jsconfuser` (`npm run dejsc`) reverses output from -[JS-Confuser](https://github.com/MichaelXF/js-confuser) 2.x, up to and including the -`high` preset. The pre-2.0 `AntiTooling` shape is still recognized as well, for -real-world samples predating the 2.0 rewrite that removed it. - -The target is a fixed sequence of AST passes, each reversing one JS-Confuser transform or -a piece of one. Several transforms are visited more than once: a transform that runs -*early* on the encode side has its output reshaped by every later encode stage, so its -decode pass often cannot match anything until the layers above it have come off. - -## What is reversed - -**Wrappers.** `Pack`'s eval wrapper, `RGF`'s eval-wrapped sub-programs — decoded -recursively through this same pipeline — and `Integrity`'s hash-guarded functions. - -**Control flow.** `ControlFlowFlattening`'s state-vector switch interpreters, -`ControlFlowGraph`, `Dispatcher`, and `Flatten`. - -**Strings and literals.** `StringConcealing`, `StringCompression`, `StringSplitting`, -`DuplicateLiteralsRemoval`, and `GlobalConcealing`. `Finalizer`'s hex and escape -re-spelling (`0x1a`, `"\x48"`) is undone too — the parser preserves the raw source -spelling of a literal and the generator prefers it, so these print back out unchanged -unless something removes it explicitly. - -**Data flow.** `VariableMasking`, `MovedDeclarations`, `Calculator`, -`ExpressionObfuscation`, and the padding `preserveFunctionLength` adds. - -**Guards and noise.** `OpaquePredicates`, `DeadCode`, `AstScrambler`, and all six `Lock` -features — antiDebug, selfDefending, dateLock, domainLock, tamperProtection, and -`invokeCountermeasures` cleanup. - -Decoding a concealed string or a masked variable slot means evaluating the obfuscator's -own runtime helper, which happens inside an [isolated-vm](https://github.com/laverdet/isolated-vm) -sandbox rather than in this process. - -## What is not reversed - -Three transforms destroy information rather than hide it, so there is nothing to -reconstruct: - -* **`RenameVariables`** and **`RenameLabels`** — a mangled name is drawn from a generator - with no relationship to the name it replaced. Nothing in the output records the - original. -* **`ObjectExtraction`** — it leaves no structural trace at all. `obj.a` becomes a bare - identifier reference, indistinguishable after renaming from any other local, and - nothing marks a group of loose variables as having once been one object. - -**`Minify`** needs no reversal of its own: its literal shortenings (`!0`, `void 0`, -`1/0`) and merged `var` declarations are undone by passes shared with the other targets, -and everything else it does is formatting or dead-code removal with nothing to recover. - -## Limits - -* An `ObjectExtraction` decoder would have something to work with only if - `renameVariables` were explicitly disabled, since the placeholder naming would then - survive into the output. That is a non-default configuration and is not implemented. -* `Finalizer` can leave a `__JS_CONFUSER_VAR__(id)` marker call when `RenameVariables` is - disabled. It exists purely to clean up after renaming rather than to obscure anything, - so it is left alone. From a995a655c8672e4cb0ef4411768c819895960619 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 04/12] feat(vm/jsconfuser-vm): read wordcode into functions, check references The frontend's first step turns the container's wordcode into typed instructions, partitions them into functions and checks every operand reference the later normalizers rely on, so a stage that rewrites the stream can re-check what it changed. Control is not derived here: the backend solves it from the switch model's cases. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- src/vm/jsconfuser-vm/opcode-table.js | 69 + .../partition-functions-input.js | 41 + src/vm/jsconfuser-vm/partition-functions.js | 258 ++++ src/vm/jsconfuser-vm/read-wordcode.js | 1136 +++++++++++++++++ .../validate-references-operands.js | 46 + src/vm/jsconfuser-vm/validate-references.js | 285 +++++ src/vm/jsconfuser-vm/wordcode-index.js | 12 + 7 files changed, 1847 insertions(+) create mode 100644 src/vm/jsconfuser-vm/opcode-table.js create mode 100644 src/vm/jsconfuser-vm/partition-functions-input.js create mode 100644 src/vm/jsconfuser-vm/partition-functions.js create mode 100644 src/vm/jsconfuser-vm/read-wordcode.js create mode 100644 src/vm/jsconfuser-vm/validate-references-operands.js create mode 100644 src/vm/jsconfuser-vm/validate-references.js create mode 100644 src/vm/jsconfuser-vm/wordcode-index.js diff --git a/src/vm/jsconfuser-vm/opcode-table.js b/src/vm/jsconfuser-vm/opcode-table.js new file mode 100644 index 00000000..ee031860 --- /dev/null +++ b/src/vm/jsconfuser-vm/opcode-table.js @@ -0,0 +1,69 @@ +// The js-confuser-vm canonical opcode values. This module is never changed: each decode creates +// its own table from it, and behaviour matching overwrites that table's values for the build. +export const CANONICAL_OPCODES = Object.freeze({ + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59, +}) + +export function createOpcodeTable() { + return { ...CANONICAL_OPCODES } +} diff --git a/src/vm/jsconfuser-vm/partition-functions-input.js b/src/vm/jsconfuser-vm/partition-functions-input.js new file mode 100644 index 00000000..5d942e41 --- /dev/null +++ b/src/vm/jsconfuser-vm/partition-functions-input.js @@ -0,0 +1,41 @@ +export const FUNCTION_SCHEMA = 'jsconfuser-vm-functions.v1' + +export class PartitionDecline extends Error { + constructor(code, message) { + super(message) + this.name = 'PartitionDecline' + this.code = code + } +} + +export function decline(code, message) { + throw new PartitionDecline(code, message) +} + +export function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) { + return value + } + Object.freeze(value) + for (const child of Object.values(value)) deepFreeze(child) + return value +} + +export function indexFrame(references) { + const descriptors = [ + ...new Map( + references.frame.descriptors.map((descriptor) => [ + descriptor.startPc, + descriptor, + ]), + ).values(), + ] + return { + frame: references.frame, + descriptors, + descriptorsByStart: new Map( + descriptors.map((descriptor) => [descriptor.startPc, descriptor]), + ), + labelReferences: references.labels.references, + } +} diff --git a/src/vm/jsconfuser-vm/partition-functions.js b/src/vm/jsconfuser-vm/partition-functions.js new file mode 100644 index 00000000..35bca16f --- /dev/null +++ b/src/vm/jsconfuser-vm/partition-functions.js @@ -0,0 +1,258 @@ +import { indexWordcode } from './wordcode-index.js' +import { + FUNCTION_SCHEMA, + PartitionDecline, + decline, + deepFreeze, + indexFrame, +} from './partition-functions-input.js' + +function assignIntervals(wordcodeData, frameData) { + const entries = [ + { + startPc: frameData.frame.mainStartPc, + kind: 'root', + descriptor: null, + }, + ...frameData.descriptors.map((descriptor) => ({ + startPc: descriptor.startPc, + kind: 'closure', + descriptor, + })), + ].sort((left, right) => left.startPc - right.startPc) + + if (entries[0].startPc !== frameData.frame.mainStartPc) { + decline('invalid-function-boundary', 'Root function is not the first entry') + } + const seenStarts = new Set() + const functions = [] + const ownerByPc = new Map() + for (let index = 0; index < entries.length; index += 1) { + const entry = entries[index] + if (seenStarts.has(entry.startPc)) { + decline( + 'invalid-function-boundary', + `Multiple function entries share pc ${entry.startPc}`, + ) + } + seenStarts.add(entry.startPc) + const endPc = entries[index + 1]?.startPc ?? wordcodeData.words.length + + const intervalInstructions = wordcodeData.instructions.filter( + ({ pc }) => pc >= entry.startPc && pc < endPc, + ) + + const functionRecord = { + id: index, + kind: entry.kind, + startPc: entry.startPc, + endPc, + paramCount: entry.descriptor?.paramCount ?? 0, + regCount: entry.descriptor?.regCount ?? frameData.frame.mainRegCount, + captureCount: entry.descriptor?.captureCount ?? 0, + hasRest: entry.descriptor?.hasRest ?? false, + parentFunctionId: null, + instructionPcs: intervalInstructions.map(({ pc }) => pc), + instructionCount: intervalInstructions.length, + descriptor: entry.descriptor + ? { + ...entry.descriptor, + captures: entry.descriptor.captures.map((capture) => ({ + ...capture, + })), + } + : null, + closureSites: [], + } + for (const instruction of intervalInstructions) { + ownerByPc.set(instruction.pc, index) + } + functions.push(functionRecord) + } + + return { functions, ownerByPc, entries } +} + +function attachClosures(wordcodeData, frameData, partition, options) { + const functionByStart = new Map( + partition.functions.map((functionRecord) => [ + functionRecord.startPc, + functionRecord, + ]), + ) + const descriptorsWithSites = new Set() + const parentSets = new Map() + const sharedParentStarts = [] + for (const instruction of wordcodeData.instructions) { + if (instruction.name !== 'MAKE_CLOSURE') continue + const target = instruction.operands[1]?.pc + const child = functionByStart.get(target) + const parentId = partition.ownerByPc.get(instruction.pc) + if (child.id === parentId) + decline( + 'invalid-function-boundary', + `MAKE_CLOSURE@${instruction.pc} does not enter a child function`, + ) + + const site = { + creationPc: instruction.pc, + parentFunctionId: parentId, + childFunctionId: child.id, + childStartPc: child.startPc, + destinationRegister: instruction.operands[0]?.index, + } + partition.functions[parentId].closureSites.push(site) + const parents = parentSets.get(child.id) ?? new Set() + parents.add(parentId) + parentSets.set(child.id, parents) + descriptorsWithSites.add(child.id) + } + for (const functionRecord of partition.functions) { + if (functionRecord.kind === 'closure') { + const parents = parentSets.get(functionRecord.id) + if (!parents || parents.size === 0) { + decline( + 'invalid-function-boundary', + `Function entry ${functionRecord.startPc} has no closure creation site`, + ) + } + if (parents.size > 1) { + // A suspect (a closure some later decoder may remove, such as a shared string helper) is + // published instead of declined: `true` admits every one, a Set only its own entries. + const suspects = options?.sharedParentSuspects + if (suspects !== true && !suspects?.has(functionRecord.startPc)) + decline( + 'invalid-function-boundary', + `Function entry ${functionRecord.startPc} has ambiguous parents`, + ) + sharedParentStarts.push(functionRecord.startPc) + } + functionRecord.parentFunctionId = Math.min(...parents) + } + } + partition.sharedParentStarts = sharedParentStarts + return { functionByStart, descriptorsWithSites } +} + +function validateDirectTargets(wordcodeData, labelReferences, partition) { + const normalized = [] + for (const reference of labelReferences) { + const sourceFunctionId = partition.ownerByPc.get(reference.pc) + const targetFunctionId = partition.ownerByPc.get(reference.target) + if ( + reference.role === 'functionEntry' && + (reference.instruction !== 'MAKE_CLOSURE' || + targetFunctionId === sourceFunctionId) + ) { + decline( + 'invalid-function-boundary', + `MAKE_CLOSURE@${reference.pc} does not enter another function`, + ) + } + if ( + reference.role !== 'functionEntry' && + targetFunctionId !== sourceFunctionId + ) { + decline( + 'cross-function-target', + `${reference.instruction}@${reference.pc} targets function ${targetFunctionId} from ${sourceFunctionId}`, + ) + } + normalized.push({ + pc: reference.pc, + instruction: reference.instruction, + operand: reference.operand, + role: reference.role, + target: reference.target, + sourceFunctionId, + targetFunctionId, + }) + } + return normalized +} + +function makeResult(wordcodeData, frameData, partition, labels) { + return deepFreeze({ + schemaVersion: FUNCTION_SCHEMA, + encoding: 'numeric-u32', + wordCount: wordcodeData.words.length, + instructionCount: wordcodeData.instructions.length, + entryPcs: partition.functions.map(({ startPc }) => startPc), + sharedParentStarts: [...partition.sharedParentStarts], + functions: partition.functions.map((functionRecord) => ({ + ...functionRecord, + instructionPcs: [...functionRecord.instructionPcs], + descriptor: functionRecord.descriptor + ? { + ...functionRecord.descriptor, + captures: functionRecord.descriptor.captures.map((capture) => ({ + ...capture, + })), + } + : null, + closureSites: functionRecord.closureSites.map((site) => ({ ...site })), + })), + ownership: wordcodeData.instructions.map((instruction) => ({ + pc: instruction.pc, + functionId: partition.ownerByPc.get(instruction.pc), + })), + labels, + frame: { + frameStart: frameData.frame.frameStart, + headerSize: frameData.frame.headerSize, + slots: { ...frameData.frame.slots }, + root: { + functionId: partition.functions[0].id, + startPc: frameData.frame.mainStartPc, + regCount: frameData.frame.mainRegCount, + }, + }, + }) +} + +function recognize(container, wordcode, references, options) { + const wordcodeData = indexWordcode(wordcode) + const frameData = indexFrame(references) + const partition = assignIntervals(wordcodeData, frameData) + attachClosures(wordcodeData, frameData, partition, options) + const labels = validateDirectTargets( + wordcodeData, + frameData.labelReferences, + partition, + ) + return makeResult(wordcodeData, frameData, partition, labels) +} + +export function diagnoseFunctionPartition( + container, + wordcode, + references, + options, +) { + try { + const result = recognize(container, wordcode, references, options) + return deepFreeze({ ok: true, result, diagnostic: null }) + } catch (error) { + if (error instanceof PartitionDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'function-partition-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} + +export function partitionFunctions(container, wordcode, references) { + return diagnoseFunctionPartition(container, wordcode, references).result +} + +export default partitionFunctions diff --git a/src/vm/jsconfuser-vm/read-wordcode.js b/src/vm/jsconfuser-vm/read-wordcode.js new file mode 100644 index 00000000..067013d6 --- /dev/null +++ b/src/vm/jsconfuser-vm/read-wordcode.js @@ -0,0 +1,1136 @@ +const WORDCODE_SCHEMA = 'jsconfuser-vm-wordcode.v1' +const UINT32_MAX = 0xffffffff + +const BINARY_REGISTER_OPCODES = new Set([ + 'ADD', + 'SUB', + 'MUL', + 'DIV', + 'MOD', + 'EXP', + 'BAND', + 'BOR', + 'BXOR', + 'SHL', + 'SHR', + 'USHR', + 'LT', + 'GT', + 'LTE', + 'GTE', + 'EQ', + 'NEQ', + 'LOOSE_EQ', + 'LOOSE_NEQ', + 'IN', + 'INSTANCEOF', +]) + +const UNARY_REGISTER_OPCODES = new Set([ + 'UNARY_NEG', + 'UNARY_POS', + 'UNARY_NOT', + 'UNARY_BITNOT', + 'TYPEOF', + 'VOID', +]) + +const THREE_REGISTER_OPCODES = new Set([ + 'GET_PROP', + 'SET_PROP', + 'DELETE_PROP', + 'DEFINE_GETTER', + 'DEFINE_SETTER', +]) + +export const FIXED_WORD_WIDTHS = Object.freeze({ + LOAD_CONST: 4, + LOAD_INT: 3, + LOAD_GLOBAL: 4, + LOAD_UPVALUE: 3, + LOAD_THIS: 2, + MOVE: 3, + STORE_GLOBAL: 4, + STORE_UPVALUE: 3, + GET_PROP: 4, + SET_PROP: 4, + DELETE_PROP: 4, + ADD: 4, + SUB: 4, + MUL: 4, + DIV: 4, + MOD: 4, + EXP: 4, + BAND: 4, + BOR: 4, + BXOR: 4, + SHL: 4, + SHR: 4, + USHR: 4, + LT: 4, + GT: 4, + LTE: 4, + GTE: 4, + EQ: 4, + NEQ: 4, + LOOSE_EQ: 4, + LOOSE_NEQ: 4, + IN: 4, + INSTANCEOF: 4, + UNARY_NEG: 3, + UNARY_POS: 3, + UNARY_NOT: 3, + UNARY_BITNOT: 3, + TYPEOF: 3, + VOID: 3, + TYPEOF_SAFE: 4, + JUMP: 2, + JUMP_IF_FALSE: 3, + JUMP_IF_TRUE: 3, + RETURN: 2, + THROW: 2, + DEFINE_GETTER: 4, + DEFINE_SETTER: 4, + FOR_IN_SETUP: 3, + FOR_IN_NEXT: 4, + TRY_SETUP: 3, + TRY_END: 1, + DEBUGGER: 1, + JUMP_REG: 2, + FINALLY_SETUP: 5, +}) + +// The decode's own opcode table (opcode-table.js) gives each word its operation, and its +// sentinel marks a spread argument count. +function opcodeNames(container) { + const names = new Map( + Object.entries(container.roles.op.values).map(([name, value]) => [ + value, + name, + ]), + ) + names.sentinel = container.roles.sentinels.values.CALL_SPREAD + return names +} + +class WordcodeDecline extends Error { + constructor(code, message) { + super(message) + this.name = 'WordcodeDecline' + this.code = code + } +} + +function decline(code, message) { + throw new WordcodeDecline(code, message) +} + +function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) { + return value + } + Object.freeze(value) + for (const child of Object.values(value)) deepFreeze(child) + return value +} + +function numericWords(container) { + return container.roles.words.values +} + +function isWord(value) { + return Number.isInteger(value) && value >= 0 && value <= UINT32_MAX +} + +function register(index) { + return { kind: 'register', index } +} + +function immediate(value) { + return { kind: 'immediate', value } +} + +function upvalueIndex(index) { + return { kind: 'upvalue-index', index } +} + +function labelTarget(value) { + return { kind: 'label-target', pc: value } +} + +function constantRef(index, key) { + return { kind: 'constant-ref', index, concealKey: key } +} + +function spreadSentinel(value) { + return { kind: 'spread-sentinel', value } +} + +function wordOperand(kind, value) { + if (kind === 'register') return register(value) + if (kind === 'label-target') return labelTarget(value) + if (kind === 'constant-index') { + return { kind, index: value } + } + if (kind === 'constant-key') { + return { kind, value } + } + if (kind === 'capture-kind' || kind === 'capture-index') { + return { kind, value } + } + if (kind === 'upvalue-index') { + return upvalueIndex(value) + } + if (kind === 'spread-sentinel') return spreadSentinel(value) + return immediate(value) +} + +function wordOperands(name, words, spread) { + const operands = words.slice(1) + if (name === 'CALL' || name === 'NEW' || name === 'CALL_METHOD') { + const method = name === 'CALL_METHOD' + const argcOffset = method ? 3 : 2 + const firstArgumentOffset = method ? 4 : 3 + const argc = operands[argcOffset] + const argumentWords = operands.slice(firstArgumentOffset) + return [ + wordOperand('register', operands[0]), + ...(method ? [wordOperand('register', operands[1])] : []), + wordOperand('register', operands[method ? 2 : 1]), + wordOperand(spread ? 'spread-sentinel' : 'immediate', argc), + ...argumentWords.map((value) => wordOperand('register', value)), + ] + } + if (name === 'MAKE_CLOSURE') { + const result = [ + wordOperand('register', operands[0]), + wordOperand('label-target', operands[1]), + wordOperand('immediate', operands[2]), + wordOperand('immediate', operands[3]), + wordOperand('immediate', operands[4]), + wordOperand('immediate', operands[5]), + ] + for (let index = 6; index < operands.length; index += 2) { + result.push( + wordOperand('capture-kind', operands[index]), + wordOperand('capture-index', operands[index + 1]), + ) + } + return result + } + if (name === 'BUILD_ARRAY') { + return [ + wordOperand('register', operands[0]), + wordOperand('immediate', operands[1]), + ...operands.slice(2).map((value) => wordOperand('register', value)), + ] + } + if (name === 'BUILD_OBJECT') { + const result = [ + wordOperand('register', operands[0]), + wordOperand('immediate', operands[1]), + ] + for (let index = 2; index < operands.length; index += 2) { + result.push( + wordOperand('register', operands[index]), + wordOperand('register', operands[index + 1]), + ) + } + return result + } + if (name === 'LOAD_CONST' || name === 'LOAD_GLOBAL') { + return [ + wordOperand('register', operands[0]), + wordOperand('constant-index', operands[1]), + wordOperand('constant-key', operands[2]), + ] + } + if (name === 'STORE_GLOBAL') { + return [ + wordOperand('constant-index', operands[0]), + wordOperand('constant-key', operands[1]), + wordOperand('register', operands[2]), + ] + } + if ( + name === 'JUMP' || + name === 'JUMP_IF_FALSE' || + name === 'JUMP_IF_TRUE' || + name === 'FOR_IN_NEXT' || + name === 'TRY_SETUP' + ) { + const labelOffset = name === 'JUMP' ? 0 : name === 'TRY_SETUP' ? 0 : 1 + return operands.map((value, index) => + wordOperand(index === labelOffset ? 'label-target' : 'register', value), + ) + } + if (name === 'FINALLY_SETUP') { + return [ + wordOperand('label-target', operands[0]), + wordOperand('register', operands[1]), + wordOperand('register', operands[2]), + wordOperand('label-target', operands[3]), + ] + } + if (name === 'LOAD_INT') { + return [wordOperand('register', operands[0]), immediate(operands[1])] + } + if (BINARY_REGISTER_OPCODES.has(name)) { + return operands.map((value) => wordOperand('register', value)) + } + if (UNARY_REGISTER_OPCODES.has(name)) { + return operands.map((value) => wordOperand('register', value)) + } + if (THREE_REGISTER_OPCODES.has(name)) { + return operands.map((value) => wordOperand('register', value)) + } + if ( + name === 'MOVE' || + name === 'LOAD_THIS' || + name === 'RETURN' || + name === 'THROW' || + name === 'JUMP_REG' || + name === 'FOR_IN_SETUP' + ) { + return operands.map((value) => wordOperand('register', value)) + } + if (name === 'LOAD_UPVALUE') { + return [wordOperand('register', operands[0]), upvalueIndex(operands[1])] + } + if (name === 'STORE_UPVALUE') { + return [upvalueIndex(operands[0]), wordOperand('register', operands[1])] + } + if (name === 'TYPEOF_SAFE') { + return [ + wordOperand('register', operands[0]), + wordOperand('constant-index', operands[1]), + wordOperand('constant-key', operands[2]), + ] + } + if (name === 'DEBUGGER' || name === 'TRY_END') return [] + return operands.map((value) => wordOperand('immediate', value)) +} + +function makeInstruction(pc, name, value, words, operands, extra = {}) { + return { + pc, + name, + opcode: { name, value }, + operands, + wordOperands: wordOperands(name, words, extra.form?.kind === 'spread'), + words: words.slice(), + width: words.length, + nextPc: pc + words.length, + ...extra, + } +} + +function checkCount(count, remaining, name, field, pc) { + if (!Number.isSafeInteger(count) || count < 0) { + decline('invalid-count', `${name} at ${pc} has an invalid ${field}`) + } + if (count > remaining) { + decline( + 'truncated-instruction', + `${name} at ${pc} is truncated in its ${field} payload`, + ) + } +} + +function checkPairCount(count, remaining, name, field, pc) { + if (!Number.isSafeInteger(count) || count < 0) { + decline('invalid-count', `${name} at ${pc} has an invalid ${field}`) + } + if (count > Math.floor(remaining / 2)) { + decline( + 'truncated-instruction', + `${name} at ${pc} is truncated in its ${field} pairs`, + ) + } +} + +function makeCursor(words, names, startPc = 0) { + let pc = startPc + let current = [] + return { + names, + sentinel: names.sentinel, + get pc() { + return pc + }, + get remaining() { + return words.length - pc + }, + begin() { + current = [] + }, + read(name, reason = 'operand') { + if (pc >= words.length) { + decline( + 'truncated-instruction', + `${name} at ${pc - current.length} is missing a ${reason}`, + ) + } + const value = words[pc] + if (!isWord(value)) { + decline( + 'invalid-word', + `Word at ${pc} is not an unsigned 32-bit integer`, + ) + } + pc += 1 + current.push(value) + return value + }, + takeCurrent() { + return current.slice() + }, + } +} + +function fixedInstruction(cursor, pc, name, value) { + const width = FIXED_WORD_WIDTHS[name] + if (!width) decline('unknown-opcode', `No width is defined for ${name}`) + const raw = [cursor.read(name, 'opcode')] + for (let i = 1; i < width; i += 1) raw.push(cursor.read(name)) + const operands = [] + const registerOperands = (values) => values.map((item) => register(item)) + + if (name === 'LOAD_CONST' || name === 'LOAD_GLOBAL') { + operands.push(register(raw[1]), constantRef(raw[2], raw[3])) + } else if (name === 'STORE_GLOBAL') { + operands.push(constantRef(raw[1], raw[2]), register(raw[3])) + } else if (name === 'TYPEOF_SAFE') { + operands.push(register(raw[1]), constantRef(raw[2], raw[3])) + } else if (name === 'LOAD_INT') { + operands.push(register(raw[1]), immediate(raw[2])) + } else if (name === 'LOAD_UPVALUE') { + operands.push(register(raw[1]), upvalueIndex(raw[2])) + } else if (name === 'STORE_UPVALUE') { + operands.push(upvalueIndex(raw[1]), register(raw[2])) + } else if (name === 'JUMP') { + operands.push(labelTarget(raw[1])) + } else if (name === 'JUMP_IF_FALSE' || name === 'JUMP_IF_TRUE') { + operands.push(register(raw[1]), labelTarget(raw[2])) + } else if (name === 'FOR_IN_NEXT') { + operands.push(register(raw[1]), register(raw[2]), labelTarget(raw[3])) + } else if (name === 'TRY_SETUP') { + operands.push(labelTarget(raw[1]), register(raw[2])) + } else if (name === 'FINALLY_SETUP') { + operands.push( + labelTarget(raw[1]), + register(raw[2]), + register(raw[3]), + labelTarget(raw[4]), + ) + } else if (BINARY_REGISTER_OPCODES.has(name)) { + operands.push(...registerOperands(raw.slice(1))) + } else if (UNARY_REGISTER_OPCODES.has(name)) { + operands.push(...registerOperands(raw.slice(1))) + } else if (THREE_REGISTER_OPCODES.has(name)) { + operands.push(...registerOperands(raw.slice(1))) + } else if ( + name === 'MOVE' || + name === 'LOAD_THIS' || + name === 'RETURN' || + name === 'THROW' || + name === 'JUMP_REG' || + name === 'FOR_IN_SETUP' + ) { + operands.push(...registerOperands(raw.slice(1))) + } + + return makeInstruction(pc, name, value, raw, operands) +} + +function readCall(cursor, pc, name, value) { + const raw = [cursor.read(name, 'opcode')] + const destination = cursor.read(name) + const receiverOrCallee = cursor.read(name) + const callee = name === 'CALL_METHOD' ? cursor.read(name) : receiverOrCallee + const argc = cursor.read(name, 'argc') + const fixedStart = name === 'CALL_METHOD' ? 5 : 4 + const spread = argc === cursor.sentinel + let argumentRegisters + if (spread) { + argumentRegisters = [cursor.read(name, 'spread array register')] + } else { + checkCount(argc, cursor.remaining, name, 'argc', pc) + argumentRegisters = [] + for (let index = 0; index < argc; index += 1) { + argumentRegisters.push(cursor.read(name, `argument register ${index}`)) + } + } + raw.push(...cursor.takeCurrent().slice(raw.length)) + + const argumentValue = spread + ? { + kind: 'spread', + sentinel: cursor.sentinel, + arrayRegister: register(argumentRegisters[0]), + } + : { + kind: 'fixed', + count: argc, + registers: argumentRegisters.map((item) => register(item)), + } + const operands = [register(destination)] + if (name === 'CALL_METHOD') operands.push(register(receiverOrCallee)) + operands.push(register(callee), spreadSentinel(argc)) + if (!spread) operands[operands.length - 1] = immediate(argc) + operands.push(...(argumentValue.registers ?? [argumentValue.arrayRegister])) + return makeInstruction(pc, name, value, raw, operands, { + form: { + kind: spread ? 'spread' : 'fixed', + argc, + sentinel: spread ? cursor.sentinel : null, + arrayRegister: spread ? argumentValue.arrayRegister : null, + }, + arguments: argumentValue, + callee: register(callee), + destination: register(destination), + receiver: name === 'CALL_METHOD' ? register(receiverOrCallee) : null, + serializedArgumentOffset: fixedStart, + }) +} + +function readClosure(cursor, pc, name, value) { + const raw = [cursor.read(name, 'opcode')] + const destination = cursor.read(name) + const startPc = cursor.read(name, 'function entry PC') + const paramCount = cursor.read(name) + const regCount = cursor.read(name) + const captureCount = cursor.read(name, 'capture count') + const hasRest = cursor.read(name, 'hasRest flag') + if (hasRest !== 0 && hasRest !== 1) { + decline( + 'invalid-descriptor', + `${name} at ${pc} has an invalid hasRest flag`, + ) + } + checkPairCount(captureCount, cursor.remaining, name, 'capture count', pc) + const captures = [] + for (let index = 0; index < captureCount; index += 1) { + const isLocal = cursor.read(name, `capture ${index} kind`) + const captureIndex = cursor.read(name, `capture ${index} index`) + if (isLocal !== 0 && isLocal !== 1) { + decline( + 'invalid-capture-pair', + `${name} at ${pc} has an invalid capture kind at pair ${index}`, + ) + } + captures.push({ + kind: isLocal === 1 ? 'local' : 'upvalue', + index: captureIndex, + isLocal: isLocal === 1, + }) + } + raw.push(...cursor.takeCurrent().slice(raw.length)) + const functionMeta = { + startPc, + paramCount, + regCount, + captureCount, + hasRest: hasRest === 1, + } + const operands = [ + register(destination), + labelTarget(startPc), + immediate(paramCount), + immediate(regCount), + immediate(captureCount), + immediate(hasRest), + ...captures, + ] + return makeInstruction(pc, name, value, raw, operands, { + form: { kind: 'closure', ...functionMeta }, + functionMeta, + captures, + capturePairs: captures, + destination: register(destination), + }) +} + +function readArray(cursor, pc, name, value) { + const raw = [cursor.read(name, 'opcode')] + const destination = cursor.read(name) + const count = cursor.read(name, 'element count') + checkCount(count, cursor.remaining, name, 'element count', pc) + const elements = [] + for (let index = 0; index < count; index += 1) { + elements.push(cursor.read(name, `element register ${index}`)) + } + raw.push(...cursor.takeCurrent().slice(raw.length)) + const elementRegisters = elements.map((item) => register(item)) + return makeInstruction( + pc, + name, + value, + raw, + [register(destination), immediate(count), ...elementRegisters], + { + form: { kind: 'array', count, elementRegisters }, + count, + destination: register(destination), + elementRegisters, + }, + ) +} + +function readObject(cursor, pc, name, value) { + const raw = [cursor.read(name, 'opcode')] + const destination = cursor.read(name) + const pairCount = cursor.read(name, 'pair count') + checkPairCount(pairCount, cursor.remaining, name, 'pair count', pc) + const pairs = [] + for (let index = 0; index < pairCount; index += 1) { + pairs.push({ + key: register(cursor.read(name, `key register ${index}`)), + value: register(cursor.read(name, `value register ${index}`)), + }) + } + raw.push(...cursor.takeCurrent().slice(raw.length)) + return makeInstruction( + pc, + name, + value, + raw, + [ + register(destination), + immediate(pairCount), + ...pairs.flatMap((pair) => [pair.key, pair.value]), + ], + { + form: { kind: 'object', pairCount, pairs }, + pairCount, + destination: register(destination), + pairs, + }, + ) +} + +function syntheticInstruction(cursor, pc, opcode, definition) { + const raw = [cursor.read('SYNTHETIC', 'opcode')] + for (let index = 0; index < definition.arity; index += 1) { + raw.push(cursor.read('SYNTHETIC')) + } + const stored = raw.slice(1) + const canonical = definition.canonicalToStored.map((index) => stored[index]) + return { + pc, + name: 'SYNTHETIC', + opcode: { name: 'SYNTHETIC', value: opcode }, + words: raw, + width: raw.length, + nextPc: pc + raw.length, + canonicalOperands: canonical, + steps: definition.steps.map((step) => ({ + ...step, + words: canonical.slice( + step.operandStart, + step.operandStart + step.operandCount, + ), + })), + } +} + +function macroInstruction(cursor, words, pc, opcode, definition) { + const raw = [cursor.read('MACRO', 'opcode')] + const steps = [] + for (const step of definition.steps) { + const virtual = [step.opcode, ...words.slice(cursor.pc)] + const parsed = parseInstruction( + makeCursor(virtual, cursor.names), + virtual, + new Map(), + ) + for (const expected of parsed.words.slice(1)) { + const actual = cursor.read('MACRO') + if (actual !== expected) { + decline('macro-step-mismatch', 'Macro operand read changed') + } + raw.push(actual) + } + steps.push({ name: step.name, opcode: step.opcode, words: parsed.words }) + } + return { + pc, + name: 'MACRO', + opcode: { name: 'MACRO', value: opcode }, + words: raw, + width: raw.length, + nextPc: pc + raw.length, + steps, + } +} + +function specializedInstruction(cursor, pc, opcode, definition) { + const raw = [cursor.read('SPECIALIZED', 'opcode')] + return { + pc, + name: 'SPECIALIZED', + opcode: { name: 'SPECIALIZED', value: opcode }, + words: raw, + width: 1, + nextPc: pc + 1, + steps: [ + { + name: definition.originalName, + opcode: definition.originalOpcode, + words: [definition.originalOpcode, ...definition.operands], + }, + ], + } +} + +function parseInstruction( + cursor, + words, + syntheticCases, + macroCases = new Map(), + specializedCases = new Map(), +) { + const pc = cursor.pc + cursor.begin() + const opcode = words[pc] + if (!isWord(opcode)) { + decline('invalid-word', `Word at ${pc} is not an unsigned 32-bit integer`) + } + const name = cursor.names.get(opcode) + if ( + !name && + !syntheticCases.has(opcode) && + !macroCases.has(opcode) && + !specializedCases.has(opcode) + ) { + decline('unknown-opcode', `Unknown opcode ${opcode} at pc ${pc}`) + } + if (name === 'PATCH') { + decline( + 'unsupported-hardening-opcode', + `PATCH at pc ${pc} is hardening-only and outside Phase 1`, + ) + } + let instruction + if (specializedCases.has(opcode)) { + instruction = specializedInstruction( + cursor, + pc, + opcode, + specializedCases.get(opcode), + ) + } else if (macroCases.has(opcode)) { + instruction = macroInstruction( + cursor, + words, + pc, + opcode, + macroCases.get(opcode), + ) + } else if (!name) { + instruction = syntheticInstruction( + cursor, + pc, + opcode, + syntheticCases.get(opcode), + ) + } else if (name === 'CALL' || name === 'CALL_METHOD' || name === 'NEW') { + instruction = readCall(cursor, pc, name, opcode) + } else if (name === 'MAKE_CLOSURE') { + instruction = readClosure(cursor, pc, name, opcode) + } else if (name === 'BUILD_ARRAY') { + instruction = readArray(cursor, pc, name, opcode) + } else if (name === 'BUILD_OBJECT') { + instruction = readObject(cursor, pc, name, opcode) + } else { + instruction = fixedInstruction(cursor, pc, name, opcode) + } + if (instruction.nextPc !== cursor.pc) { + decline( + 'reader-internal-error', + `${name ?? 'SYNTHETIC'} at ${pc} did not consume its declared width`, + ) + } + return instruction +} + +function parseWords( + words, + names, + syntheticCases = new Map(), + macroCases = new Map(), + specializedCases = new Map(), +) { + const cursor = makeCursor(words, names) + const instructions = [] + while (cursor.remaining > 0) { + instructions.push( + parseInstruction( + cursor, + words, + syntheticCases, + macroCases, + specializedCases, + ), + ) + } + if (cursor.pc !== words.length) { + decline('trailing-word', 'Word stream was not consumed completely') + } + return instructions +} + +export function diagnoseWordcodeInstruction(container, pc) { + try { + const words = numericWords(container) + if (!Number.isSafeInteger(pc) || pc < 0 || pc >= words.length) { + decline('invalid-pc', 'Instruction PC is outside the word stream') + } + const cursor = makeCursor(words, opcodeNames(container), pc) + return deepFreeze({ + ok: true, + result: parseInstruction(cursor, words, new Map()), + diagnostic: null, + }) + } catch (error) { + if (error instanceof WordcodeDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'word-reader-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} + +// PATCH traversal reads only reached words, using the same source-proved added-opcode +// definitions as the complete stream readers. Original widths remain intact. +export function diagnoseObservedWordcodeInstruction( + container, + pc, + addedOpcodes = {}, +) { + try { + const words = numericWords(container) + const antiCases = opcodeMap(addedOpcodes.anti?.syntheticCases ?? []) + const aliasCases = opcodeMap(addedOpcodes.alias?.aliases ?? []) + const cases = new Map(antiCases) + for (const entry of aliasCases.values()) { + cases.set(entry.opcode, { + ...entry, + steps: [ + { + name: entry.originalName, + operandStart: 0, + operandCount: entry.arity, + }, + ], + }) + } + const instruction = parseInstruction( + makeCursor(words, opcodeNames(container), pc), + words, + cases, + opcodeMap(addedOpcodes.macro?.macros ?? []), + opcodeMap(addedOpcodes.specialized?.specialized ?? []), + ) + const alias = aliasCases.get(instruction.opcode.value) + if (alias) { + instruction.name = 'ALIAS' + instruction.steps = [ + { + name: alias.originalName, + words: [alias.originalOpcode, ...instruction.canonicalOperands], + }, + ] + } + return deepFreeze({ ok: true, result: instruction, diagnostic: null }) + } catch (error) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: + error instanceof WordcodeDecline ? error.code : 'word-reader-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} + +// Runtime observers establish these definitions from source before stream parsing. +function opcodeMap(entries) { + return new Map(entries.map((entry) => [entry.opcode, entry])) +} + +export function diagnoseAntiWordStream(container, observation) { + try { + const words = numericWords(container) + const cases = opcodeMap(observation.syntheticCases) + const instructions = parseWords(words, opcodeNames(container), cases) + if (!instructions.some(({ name }) => name === 'SYNTHETIC')) { + decline( + 'no-synthetic-instructions', + 'The word stream has no synthetic opcode', + ) + } + return deepFreeze({ + ok: true, + result: { + schemaVersion: 'jsconfuser-vm-anti-word-stream-observation.v1', + wordCount: words.length, + instructions, + }, + diagnostic: null, + }) + } catch (error) { + if (error instanceof WordcodeDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'anti-word-stream-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} + +export function diagnoseAliasedWordStream( + container, + observation, + { + antiObservation = null, + specializedObservation = null, + macroObservation = null, + } = {}, +) { + try { + const words = numericWords(container) + const antiCases = antiObservation + ? opcodeMap(antiObservation.syntheticCases) + : new Map() + const specializedCases = specializedObservation + ? opcodeMap(specializedObservation.specialized) + : new Map() + const macroCases = macroObservation + ? opcodeMap(macroObservation.macros) + : new Map() + const cases = new Map(antiCases) + for (const entry of observation.aliases) { + cases.set(entry.opcode, { + ...entry, + steps: [ + { + name: entry.originalName, + operandStart: 0, + operandCount: entry.arity, + }, + ], + }) + } + const instructions = parseWords( + words, + opcodeNames(container), + cases, + macroCases, + specializedCases, + ) + if ( + !instructions.some( + ({ name, opcode }) => + name === 'SYNTHETIC' && !antiCases.has(opcode.value), + ) + ) { + decline('no-alias-instructions', 'The word stream has no alias opcode') + } + return deepFreeze({ + ok: true, + result: { + schemaVersion: 'jsconfuser-vm-aliased-word-stream-observation.v1', + wordCount: words.length, + instructions, + }, + diagnostic: null, + }) + } catch (error) { + if (error instanceof WordcodeDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'aliased-word-stream-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} + +export function diagnoseMacroWordStream( + container, + observation, + specializedObservation, + antiObservation, +) { + try { + const words = numericWords(container) + const antiCases = antiObservation + ? opcodeMap(antiObservation.syntheticCases) + : new Map() + const specializedCases = specializedObservation + ? opcodeMap(specializedObservation.specialized) + : new Map() + const cases = opcodeMap(observation.macros) + const instructions = parseWords( + words, + opcodeNames(container), + antiCases, + cases, + specializedCases, + ) + if (!instructions.some(({ name }) => name === 'MACRO')) { + decline('no-macro-instructions', 'The word stream has no macro opcode') + } + return deepFreeze({ + ok: true, + result: { + schemaVersion: 'jsconfuser-vm-macro-word-stream-observation.v1', + wordCount: words.length, + instructions, + }, + diagnostic: null, + }) + } catch (error) { + if (error instanceof WordcodeDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'macro-word-stream-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} + +export function diagnoseSpecializedWordStream( + container, + observation, + antiObservation, +) { + try { + const words = numericWords(container) + const antiCases = antiObservation + ? opcodeMap(antiObservation.syntheticCases) + : new Map() + const cases = opcodeMap(observation.specialized) + const instructions = parseWords( + words, + opcodeNames(container), + antiCases, + new Map(), + cases, + ) + if (!instructions.some(({ name }) => name === 'SPECIALIZED')) { + decline( + 'no-specialized-instructions', + 'The word stream has no specialized opcode', + ) + } + return deepFreeze({ + ok: true, + result: { + schemaVersion: 'jsconfuser-vm-specialized-word-stream-observation.v1', + wordCount: words.length, + instructions, + }, + diagnostic: null, + }) + } catch (error) { + if (error instanceof WordcodeDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'specialized-word-stream-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} + +function recognize(container) { + const words = numericWords(container) + const instructions = parseWords(words, opcodeNames(container)) + return deepFreeze({ + schemaVersion: WORDCODE_SCHEMA, + encoding: 'numeric-u32', + words: words.slice(), + wordCount: words.length, + instructions, + instructionCount: instructions.length, + consumedWords: words.length, + nextPc: words.length, + }) +} + +export function diagnoseWordcode(container) { + try { + const result = recognize(container) + return deepFreeze({ ok: true, result, diagnostic: null }) + } catch (error) { + if (error instanceof WordcodeDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'word-reader-error', + message: error.message, + }, + }) + } +} + +export function readWordcode(container) { + return diagnoseWordcode(container).result +} + +export default readWordcode diff --git a/src/vm/jsconfuser-vm/validate-references-operands.js b/src/vm/jsconfuser-vm/validate-references-operands.js new file mode 100644 index 00000000..ac5c5422 --- /dev/null +++ b/src/vm/jsconfuser-vm/validate-references-operands.js @@ -0,0 +1,46 @@ +export const REFERENCE_SCHEMA = 'jsconfuser-vm-references.v1' + +export const CANONICAL_SLOTS = Object.freeze({ + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7, +}) + +export class ReferenceDecline extends Error { + constructor(code, message) { + super(message) + this.name = 'ReferenceDecline' + this.code = code + } +} + +export function decline(code, message) { + throw new ReferenceDecline(code, message) +} + +export function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) { + return value + } + Object.freeze(value) + for (const child of Object.values(value)) deepFreeze(child) + return value +} + +export function containerData(container) { + return { + pool: container.roles.pool.values, + words: container.roles.words.values, + scalars: Object.fromEntries( + Object.entries(container.roles.scalars).map(([name, role]) => [ + name, + role.value, + ]), + ), + } +} diff --git a/src/vm/jsconfuser-vm/validate-references.js b/src/vm/jsconfuser-vm/validate-references.js new file mode 100644 index 00000000..6062baf7 --- /dev/null +++ b/src/vm/jsconfuser-vm/validate-references.js @@ -0,0 +1,285 @@ +import { indexWordcode } from './wordcode-index.js' +import { + CANONICAL_SLOTS, + REFERENCE_SCHEMA, + ReferenceDecline, + decline, + deepFreeze, + containerData, +} from './validate-references-operands.js' + +function validateFrameSlots(input, wordcodeData) { + const { MAIN_START_PC: mainStartPc, MAIN_REG_COUNT: mainRegCount } = + input.scalars + const headerSize = input.scalars.HEADER_SIZE + const frameStart = input.scalars.FRAME_START + const slots = CANONICAL_SLOTS + const boundarySet = new Set(wordcodeData.boundaries) + const rootFrameSize = headerSize + mainRegCount + const rootRegisterBase = frameStart + headerSize + const rootFrameEnd = frameStart + rootFrameSize + return { + boundarySet, + frame: { + frameStart, + headerSize, + slots: { ...slots }, + mainStartPc, + mainRegCount, + root: { + frameBase: frameStart, + frameSize: rootFrameSize, + registerBase: rootRegisterBase, + registerWindow: { start: rootRegisterBase, end: rootFrameEnd }, + frameEnd: rootFrameEnd, + }, + }, + } +} + +function descriptorRecord(instruction) { + const metadata = instruction.functionMeta + const captures = instruction.captures + return { + creationPc: instruction.pc, + startPc: metadata.startPc, + paramCount: metadata.paramCount, + regCount: metadata.regCount, + captureCount: metadata.captureCount, + hasRest: metadata.hasRest, + captures: captures.map(({ kind, index, isLocal }) => ({ + kind, + index, + isLocal, + })), + } +} + +function validateDescriptors(wordcodeData, frameData) { + const closureInstructions = wordcodeData.instructions.filter( + ({ name }) => name === 'MAKE_CLOSURE', + ) + const descriptors = [] + let maxRegisterCount = frameData.frame.mainRegCount + let maxCaptureCount = 0 + for (const instruction of closureInstructions) { + const metadata = instruction.functionMeta + if (!frameData.boundarySet.has(metadata.startPc)) { + decline( + 'invalid-label-reference', + `MAKE_CLOSURE@${instruction.pc} has an invalid entry PC`, + ) + } + const captures = instruction.captures.map(({ kind, index, isLocal }) => ({ + kind, + index, + isLocal, + })) + const record = { + ...descriptorRecord({ ...instruction, functionMeta: metadata, captures }), + captures, + frameSize: frameData.frame.headerSize + metadata.regCount, + registerBaseOffset: frameData.frame.headerSize, + } + descriptors.push(record) + maxRegisterCount = Math.max(maxRegisterCount, metadata.regCount) + maxCaptureCount = Math.max(maxCaptureCount, metadata.captureCount) + } + for (const descriptor of descriptors) { + for (const capture of descriptor.captures) { + if (capture.kind === 'local' && capture.index >= maxRegisterCount) { + decline( + 'invalid-register-reference', + `MAKE_CLOSURE@${descriptor.creationPc} captures r${capture.index} outside the known frame bounds`, + ) + } + if (capture.kind === 'upvalue' && capture.index >= maxCaptureCount) { + decline( + 'invalid-descriptor', + `MAKE_CLOSURE@${descriptor.creationPc} captures upvalue ${capture.index} outside the known descriptor bounds`, + ) + } + } + } + return { descriptors, maxRegisterCount, maxCaptureCount } +} + +function labelRole(name, index) { + if (name === 'JUMP') return 'target' + if (name === 'JUMP_IF_FALSE' || name === 'JUMP_IF_TRUE') return 'target' + if (name === 'FOR_IN_NEXT') return 'exit' + if (name === 'TRY_SETUP') return 'handler' + if (name === 'FINALLY_SETUP') return index === 0 ? 'finally' : 'throwPad' + if (name === 'MAKE_CLOSURE') return 'functionEntry' + return 'label' +} + +function collectReferences( + containerData, + wordcodeData, + frameData, + descriptors, +) { + const constantReferences = [] + const registerReferences = [] + const labelReferences = [] + const captureLimit = descriptors.maxCaptureCount + const { pool } = containerData + const directLabelIndexes = { + JUMP: [0], + JUMP_IF_FALSE: [1], + JUMP_IF_TRUE: [1], + FOR_IN_NEXT: [2], + TRY_SETUP: [0], + FINALLY_SETUP: [0, 3], + MAKE_CLOSURE: [1], + } + + for (const instruction of wordcodeData.instructions) { + instruction.operands.forEach((operand, operandIndex) => { + if (operand.kind === 'register') { + // Bounded by the owning descriptor in E, on reachable code only: C has no owners and no + // reachability, and it reads streams that still hold selfModifying's suspects (registers + // 0..24, whatever the frame). + registerReferences.push({ + pc: instruction.pc, + instruction: instruction.name, + operand: operandIndex, + index: operand.index, + }) + } else if (operand.kind === 'constant-ref') { + if (operand.index >= pool.length || operand.concealKey !== 0) { + decline( + 'invalid-constant-reference', + `${instruction.name}@${instruction.pc} has an invalid constant pair`, + ) + } + if ( + (instruction.name === 'LOAD_GLOBAL' || + instruction.name === 'STORE_GLOBAL' || + instruction.name === 'TYPEOF_SAFE') && + typeof pool[operand.index] !== 'string' + ) { + decline( + 'invalid-constant-reference', + `${instruction.name}@${instruction.pc} requires a string constant name`, + ) + } + constantReferences.push({ + pc: instruction.pc, + instruction: instruction.name, + operand: operandIndex, + index: operand.index, + concealKey: operand.concealKey, + }) + } else if (operand.kind === 'upvalue-index') { + if (operand.index >= captureLimit) { + decline( + 'invalid-descriptor', + `${instruction.name}@${instruction.pc} references an unknown upvalue bound`, + ) + } + } else if (operand.kind === 'local' || operand.kind === 'upvalue') { + // Capture-pair bounds and coherence are checked from functionMeta below. + // Their owner function is deliberately left to partitionFunctions. + } + }) + + const actualLabels = directLabelIndexes[instruction.name] ?? [] + for (const operandIndex of actualLabels) { + const target = instruction.operands[operandIndex].pc + if (!frameData.boundarySet.has(target)) { + decline( + 'invalid-label-reference', + `${instruction.name}@${instruction.pc} targets non-boundary pc ${target}`, + ) + } + labelReferences.push({ + pc: instruction.pc, + instruction: instruction.name, + operand: operandIndex, + role: labelRole(instruction.name, operandIndex), + target, + }) + } + } + return { constantReferences, registerReferences, labelReferences } +} + +function makeResult( + containerData, + wordcodeData, + frameData, + descriptorData, + refs, +) { + const frame = { + ...frameData.frame, + descriptors: descriptorData.descriptors, + } + return deepFreeze({ + schemaVersion: REFERENCE_SCHEMA, + encoding: 'numeric-u32', + wordCount: wordcodeData.words.length, + instructionCount: wordcodeData.instructions.length, + constants: { + poolSize: containerData.pool.length, + values: containerData.pool.slice(), + references: refs.constantReferences, + }, + registers: { + rootCount: frameData.frame.mainRegCount, + maxCount: descriptorData.maxRegisterCount, + maxCaptureCount: descriptorData.maxCaptureCount, + references: refs.registerReferences, + }, + labels: { + boundaries: + frameData.frame.mainStartPc === 0 + ? [...frameData.boundarySet].sort((left, right) => left - right) + : [...frameData.boundarySet], + references: refs.labelReferences, + }, + frame, + }) +} + +function recognize(container, wordcode) { + const input = containerData(container) + const wordcodeData = indexWordcode(wordcode) + const frameData = validateFrameSlots(input, wordcodeData) + const descriptorData = validateDescriptors(wordcodeData, frameData) + const refs = collectReferences(input, wordcodeData, frameData, descriptorData) + return makeResult(input, wordcodeData, frameData, descriptorData, refs) +} + +export function diagnoseReferences(container, wordcode) { + try { + const result = recognize(container, wordcode) + return deepFreeze({ ok: true, result, diagnostic: null }) + } catch (error) { + if (error instanceof ReferenceDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'reference-validator-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} + +export function validateReferencesResult(container, wordcode) { + return diagnoseReferences(container, wordcode).result +} + +export const validateReferences = validateReferencesResult + +export default validateReferencesResult diff --git a/src/vm/jsconfuser-vm/wordcode-index.js b/src/vm/jsconfuser-vm/wordcode-index.js new file mode 100644 index 00000000..207b25ab --- /dev/null +++ b/src/vm/jsconfuser-vm/wordcode-index.js @@ -0,0 +1,12 @@ +// Index the successful readWordcode record without parsing or validating it again. +export function indexWordcode(wordcode) { + return { + words: wordcode.words, + instructions: wordcode.instructions, + byPc: new Map( + wordcode.instructions.map((instruction) => [instruction.pc, instruction]), + ), + boundaries: wordcode.instructions.map(({ pc }) => pc), + boundarySet: new Set(wordcode.instructions.map(({ pc }) => pc)), + } +} From 565fda18684089fe72af15e7a584456fde94baca Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 05/12] feat(visitor/normalize-source): prepare VM sources for container location Container location matches the runtime by structure, and the same control body arrives braced or bare depending on the encoder's minify and formatting; this visitor gives control bodies one form first, so the locator matches one spelling. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- src/visitor/jsconfuser-vm/normalize-source.js | 91 +++++++++++++++++++ .../jsconfuser-vm/normalize-source.test.js | 78 ++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 src/visitor/jsconfuser-vm/normalize-source.js create mode 100644 test/visitor/jsconfuser-vm/normalize-source.test.js diff --git a/src/visitor/jsconfuser-vm/normalize-source.js b/src/visitor/jsconfuser-vm/normalize-source.js new file mode 100644 index 00000000..2b1aa914 --- /dev/null +++ b/src/visitor/jsconfuser-vm/normalize-source.js @@ -0,0 +1,91 @@ +import { parse } from '@babel/parser' +import generate from '@babel/generator' +import traverse from '@babel/traverse' +import * as t from '@babel/types' +import { createUncomputeMember } from '../atomic/uncompute-member.js' +import { createUncomputePropertyKey } from '../atomic/uncompute-property-key.js' + +const SIMPLE_STATEMENTS = new Set([ + 'ExpressionStatement', + 'ReturnStatement', + 'ThrowStatement', + 'BreakStatement', + 'ContinueStatement', +]) +const LOOP_TYPES = new Set([ + 'ForStatement', + 'ForInStatement', + 'ForOfStatement', + 'WhileStatement', + 'DoWhileStatement', +]) + +// A read-only view also used on reference templates. Never erase a lexical +// declaration, directive, nested conditional (dangling else), or function body. +export function simpleControlBody(node) { + return t.isBlockStatement(node) && + node.body.length === 1 && + !node.directives?.length && + SIMPLE_STATEMENTS.has(node.body[0].type) + ? node.body[0] + : node +} + +export function isControlBodyKey(type, key) { + return type === 'IfStatement' + ? key === 'consequent' || key === 'alternate' + : LOOP_TYPES.has(type) && key === 'body' +} + +export function createNormalizeControlBodies(onChange = () => {}) { + let changed = false + return { + Program: { + enter() { + changed = false + }, + exit(path) { + if (changed) path.scope.crawl() + }, + }, + 'IfStatement|ForStatement|ForInStatement|ForOfStatement|WhileStatement|DoWhileStatement': + { + exit(path) { + const keys = path.isIfStatement() + ? ['consequent', 'alternate'] + : ['body'] + for (const key of keys) { + const original = path.node[key] + const body = simpleControlBody(original) + if (body === original) continue + path.get(key).replaceWith(t.cloneNode(body, true)) + changed = true + onChange() + } + }, + }, + } +} + +export function normalizeVmAst(ast) { + let changed = false + const note = () => { + changed = true + } + traverse(ast, createUncomputeMember(note)) + traverse(ast, createUncomputePropertyKey(note)) + traverse(ast, createNormalizeControlBodies(note)) + return changed +} + +export default function normalizeVmInput(source) { + if (typeof source !== 'string') return source + let ast + try { + ast = parse(source, { errorRecovery: false, sourceType: 'script' }) + } catch (error) { + if (error instanceof SyntaxError) return source + throw error + } + return normalizeVmAst(ast) ? generate(ast).code : source +} diff --git a/test/visitor/jsconfuser-vm/normalize-source.test.js b/test/visitor/jsconfuser-vm/normalize-source.test.js new file mode 100644 index 00000000..209eabd5 --- /dev/null +++ b/test/visitor/jsconfuser-vm/normalize-source.test.js @@ -0,0 +1,78 @@ +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { parse } from '@babel/parser' +import generate from '@babel/generator' +import traverse from '@babel/traverse' +import normalizeVmInput, { + createNormalizeControlBodies, + normalizeVmAst, +} from '../../../src/visitor/jsconfuser-vm/normalize-source.js' +import { expectConsistentState } from '../../helper.js' + +function execute(source) { + const context = {} + vm.runInNewContext(source, context, { timeout: 1000 }) + return JSON.stringify(context.output) +} + +function normalize(source) { + const ast = parse(source) + normalizeVmAst(ast) + expectConsistentState(ast) + const output = generate(ast).code + expect(execute(output)).toBe(execute(source)) + return ast +} + +// Declarator placement and function-expression form are left to the consumer, +// which selects a role by its single defining write rather than by spelling. +test('leaves declaration placement and function-expression form as written', () => { + const source = ` + var x, f; output = [typeof x]; + x = 3; + f = function hidden(v) { return v + x; }; + output.push(f(1)); + ` + const ast = normalize(source) + expect(generate(ast).code).toBe(generate(parse(source)).code) +}) + +test('reuses guarded key rewrites while preserving special object and class keys', () => { + const ast = normalize(` + var object = { ["__proto__"]: 3, ["safe"]: 5 }; + class C { ["constructor"]() { return 7; } } + output = [object["safe"], object["__proto__"], new C()["constructor"]()]; + `) + const properties = ast.program.body[0].declarations[0].init.properties + expect(properties[0].computed).toBe(true) + expect(properties[1].computed).toBe(false) + expect(ast.program.body[1].body.body[0].computed).toBe(true) +}) + +test('simple control bodies preserve lexical blocks and dangling-else grouping', () => { + const source = ` + function f(x) { + if (x) { return 1; } else { return 2; } + } + if (true) { let x = 4; } + if (true) { if (false) output = 99; } else output = 100; + output = [f(true), f(false)]; + ` + const ast = parse(source) + traverse(ast, createNormalizeControlBodies()) + expectConsistentState(ast) + expect(ast.program.body[0].body.body[0].consequent.type).toBe( + 'ReturnStatement', + ) + expect(ast.program.body[1].consequent.type).toBe('BlockStatement') + expect(ast.program.body[2].consequent.type).toBe('BlockStatement') + expect(execute(generate(ast).code)).toBe(execute(source)) +}) + +test('normalization is idempotent and does not recover malformed or non-script admission', () => { + const source = 'var x; x = { ["safe"]: 5 }; output = x["safe"];' + const once = normalizeVmInput(source) + expect(normalizeVmInput(once)).toBe(once) + for (const invalid of ['function (', 'return 5;', 'export const x = 3;']) + expect(normalizeVmInput(invalid)).toBe(invalid) +}) From 4a5d32cdae8279e11642a2330e27d1e79629c21b Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 06/12] feat(vm/jsconfuser-vm): locate the container, identify handlers by effects The runtime's names, layout and handler order are randomized, so the container, boot payload and handler table are found by structure, and each handler is identified by what it does to the machine state rather than by its spelling. The fixtures are encoder output with their generation records. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- .../jsconfuser-vm/container-role-helpers.js | 271 +++++ src/vm/jsconfuser-vm/extract-container.js | 75 ++ src/vm/jsconfuser-vm/handler-effects.js | 429 +++++++ src/vm/jsconfuser-vm/handler-matchers.js | 204 ++++ src/vm/jsconfuser-vm/handler-meaning.js | 250 ++++ src/vm/jsconfuser-vm/identify-handlers.js | 188 +++ src/vm/jsconfuser-vm/locate-boot-payload.js | 267 +++++ src/vm/jsconfuser-vm/locate-handlers.js | 1046 +++++++++++++++++ .../materialize-encoded-bytecode.js | 24 + .../minified-runtime-container.js | 51 + .../jsconfuser-vm/runtime-fragment-pattern.js | 22 + .../jsconfuser-vm/runtime-source-signature.js | 45 + .../encoded.js | 1019 ++++++++++++++++ .../oracle.json | 44 + .../source.js | 9 + .../composition-closure-upvalue/encoded.js | 1019 ++++++++++++++++ .../composition-closure-upvalue/oracle.json | 38 + .../composition-closure-upvalue/source.js | 8 + .../encoded.js | 1019 ++++++++++++++++ .../oracle.json | 39 + .../source.js | 12 + .../encoded.js | 1019 ++++++++++++++++ .../oracle.json | 42 + .../source.js | 10 + .../composition-receiver-mutation/encoded.js | 1019 ++++++++++++++++ .../composition-receiver-mutation/oracle.json | 39 + .../composition-receiver-mutation/source.js | 8 + .../caught-return-through-finally/encoded.js | 1019 ++++++++++++++++ .../caught-return-through-finally/oracle.json | 62 + .../caught-return-through-finally/source.js | 1 + .../focused-call-method-spread/encoded.js | 1019 ++++++++++++++++ .../focused-call-method-spread/oracle.json | 37 + .../focused-call-method-spread/source.js | 1 + .../focused/focused-new-spread/encoded.js | 1019 ++++++++++++++++ .../focused/focused-new-spread/oracle.json | 34 + .../focused/focused-new-spread/source.js | 1 + .../encoded.js | 1019 ++++++++++++++++ .../oracle.json | 30 + .../focused-store-global-assignment/source.js | 1 + .../encoded.js | 1019 ++++++++++++++++ .../oracle.json | 34 + .../focused-unary-positive-coercion/source.js | 1 + .../protected-conditional-catch/encoded.js | 1019 ++++++++++++++++ .../protected-conditional-catch/oracle.json | 61 + .../protected-conditional-catch/source.js | 1 + .../focused/return-through-finally/encoded.js | 1019 ++++++++++++++++ .../return-through-finally/oracle.json | 60 + .../focused/return-through-finally/source.js | 1 + .../focused/terminal-switch-arms/encoded.js | 1019 ++++++++++++++++ .../focused/terminal-switch-arms/oracle.json | 60 + .../focused/terminal-switch-arms/source.js | 1 + .../closure-invalid-capture/encoded.js | 1019 ++++++++++++++++ .../closure-invalid-capture/oracle.json | 27 + .../closure-invalid-capture/source.js | 3 + .../negative/control-stale-target/encoded.js | 1019 ++++++++++++++++ .../negative/control-stale-target/oracle.json | 28 + .../negative/control-stale-target/source.js | 3 + .../negative/extra-trailing-word/encoded.js | 1019 ++++++++++++++++ .../negative/extra-trailing-word/oracle.json | 27 + .../negative/extra-trailing-word/source.js | 1 + .../negative/finally-stale-target/encoded.js | 1019 ++++++++++++++++ .../negative/finally-stale-target/oracle.json | 27 + .../negative/finally-stale-target/source.js | 3 + .../handler-invalid-target/encoded.js | 1019 ++++++++++++++++ .../handler-invalid-target/oracle.json | 27 + .../negative/handler-invalid-target/source.js | 3 + .../encoded.js | 1019 ++++++++++++++++ .../oracle.json | 27 + .../source.js | 1 + .../missing-container-role/encoded.js | 1019 ++++++++++++++++ .../missing-container-role/oracle.json | 27 + .../negative/missing-container-role/source.js | 1 + .../negative/ordinary-near-miss/encoded.js | 2 + .../negative/ordinary-near-miss/oracle.json | 27 + .../negative/ordinary-near-miss/source.js | 2 + .../reference-invalid-constant/encoded.js | 1019 ++++++++++++++++ .../reference-invalid-constant/oracle.json | 27 + .../reference-invalid-constant/source.js | 1 + .../reference-invalid-label/encoded.js | 1019 ++++++++++++++++ .../reference-invalid-label/oracle.json | 28 + .../reference-invalid-label/source.js | 3 + .../reference-invalid-ownership/encoded.js | 1019 ++++++++++++++++ .../reference-invalid-ownership/oracle.json | 27 + .../reference-invalid-ownership/source.js | 3 + .../negative/truncated-wordcode/encoded.js | 1019 ++++++++++++++++ .../negative/truncated-wordcode/oracle.json | 27 + .../negative/truncated-wordcode/source.js | 1 + .../corpus/negative/unknown-opcode/encoded.js | 1019 ++++++++++++++++ .../negative/unknown-opcode/oracle.json | 27 + .../corpus/negative/unknown-opcode/source.js | 1 + .../raw/f-accessors-and-methods/encoded.js | 1019 ++++++++++++++++ .../raw/f-accessors-and-methods/oracle.json | 42 + .../raw/f-accessors-and-methods/source.js | 5 + .../corpus/raw/f-arrow-forms/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-arrow-forms/oracle.json | 42 + .../corpus/raw/f-arrow-forms/source.js | 4 + .../raw/f-arrow-lexical-state/encoded.js | 1019 ++++++++++++++++ .../raw/f-arrow-lexical-state/oracle.json | 35 + .../raw/f-arrow-lexical-state/source.js | 4 + .../corpus/raw/f-branching/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-branching/oracle.json | 34 + .../fixtures/corpus/raw/f-branching/source.js | 5 + .../corpus/raw/f-closures-state/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-closures-state/oracle.json | 35 + .../corpus/raw/f-closures-state/source.js | 3 + .../corpus/raw/f-debugger-order/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-debugger-order/oracle.json | 32 + .../corpus/raw/f-debugger-order/source.js | 5 + .../raw/f-expression-mutation/encoded.js | 1019 ++++++++++++++++ .../raw/f-expression-mutation/oracle.json | 45 + .../raw/f-expression-mutation/source.js | 4 + .../corpus/raw/f-expression-values/encoded.js | 1019 ++++++++++++++++ .../raw/f-expression-values/oracle.json | 43 + .../corpus/raw/f-expression-values/source.js | 4 + .../corpus/raw/f-finally-abrupt/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-finally-abrupt/oracle.json | 52 + .../corpus/raw/f-finally-abrupt/source.js | 7 + .../raw/f-for-in-enumeration/encoded.js | 1019 ++++++++++++++++ .../raw/f-for-in-enumeration/oracle.json | 40 + .../corpus/raw/f-for-in-enumeration/source.js | 5 + .../raw/f-functions-call-return/encoded.js | 1019 ++++++++++++++++ .../raw/f-functions-call-return/oracle.json | 40 + .../raw/f-functions-call-return/source.js | 4 + .../corpus/raw/f-functions-params/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-functions-params/oracle.json | 45 + .../corpus/raw/f-functions-params/source.js | 3 + .../corpus/raw/f-functions-rest/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-functions-rest/oracle.json | 48 + .../corpus/raw/f-functions-rest/source.js | 3 + .../corpus/raw/f-global-resolution/encoded.js | 1019 ++++++++++++++++ .../raw/f-global-resolution/oracle.json | 36 + .../corpus/raw/f-global-resolution/source.js | 4 + .../corpus/raw/f-instanceof-errors/encoded.js | 1019 ++++++++++++++++ .../raw/f-instanceof-errors/oracle.json | 33 + .../corpus/raw/f-instanceof-errors/source.js | 3 + .../corpus/raw/f-labeled-control/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-labeled-control/oracle.json | 34 + .../corpus/raw/f-labeled-control/source.js | 5 + .../corpus/raw/f-literals-order/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-literals-order/oracle.json | 80 ++ .../corpus/raw/f-literals-order/source.js | 8 + .../corpus/raw/f-loop-abrupt/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-loop-abrupt/oracle.json | 39 + .../corpus/raw/f-loop-abrupt/source.js | 5 + .../corpus/raw/f-loop-forms/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-loop-forms/oracle.json | 35 + .../corpus/raw/f-loop-forms/source.js | 4 + .../corpus/raw/f-regexp-state/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-regexp-state/oracle.json | 40 + .../corpus/raw/f-regexp-state/source.js | 7 + .../corpus/raw/f-short-circuit/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-short-circuit/oracle.json | 41 + .../corpus/raw/f-short-circuit/source.js | 7 + .../corpus/raw/f-spread-order/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-spread-order/oracle.json | 37 + .../corpus/raw/f-spread-order/source.js | 4 + .../corpus/raw/f-switch-flow/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-switch-flow/generation.json | 11 + .../corpus/raw/f-switch-flow/oracle.json | 71 ++ .../corpus/raw/f-switch-flow/source.js | 4 + .../corpus/raw/f-template-literals/encoded.js | 1019 ++++++++++++++++ .../raw/f-template-literals/oracle.json | 34 + .../corpus/raw/f-template-literals/source.js | 1 + .../corpus/raw/f-this-explicit/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-this-explicit/oracle.json | 35 + .../corpus/raw/f-this-explicit/source.js | 5 + .../corpus/raw/f-this-receiver/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-this-receiver/oracle.json | 35 + .../corpus/raw/f-this-receiver/source.js | 5 + .../corpus/raw/f-throw-catch/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/f-throw-catch/oracle.json | 47 + .../corpus/raw/f-throw-catch/source.js | 4 + .../corpus/raw/program-cash/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/program-cash/oracle.json | 67 ++ .../corpus/raw/program-cash/source.js | 2 + .../corpus/raw/program-sha256/encoded.js | 1019 ++++++++++++++++ .../corpus/raw/program-sha256/oracle.json | 74 ++ .../corpus/raw/program-sha256/source.js | 459 ++++++++ .../exceptions/encoded-169.js | 17 + .../exceptions/encoded-170.js | 19 + .../exceptions/encoded-171.js | 17 + .../exceptions/source.js | 1 + .../fixtures/outer-layers/compact.encoded.js | 1 + .../fixtures/outer-layers/generation.json | 48 + .../fixtures/outer-layers/minify.encoded.js | 1 + .../moved-declarations.encoded.js | 1 + .../outer-layers/object-extraction.encoded.js | 1 + .../fixtures/outer-layers/source.js | 7 + .../outer-layers/variable-masking.encoded.js | 1 + .../fixtures/outer-layers/vm.encoded.js | 1021 ++++++++++++++++ .../fixtures/reference/encoded.debug.js | 1029 ++++++++++++++++ .../fixtures/reference/encoded.js | 1019 ++++++++++++++++ .../fixtures/reference/oracle.json | 20 + .../fixtures/reference/source.js | 1 + .../class-minify-aliased.encoded.js | 18 + .../class-minify-specialized.encoded.js | 20 + .../class-minify-table-shuffle.encoded.js | 21 + .../runtime-layers/class-minify.encoded.js | 17 + .../fixtures/runtime-layers/generation.json | 96 ++ .../runtime-layers/minify-aliased.encoded.js | 17 + .../minify-randomized.encoded.js | 16 + .../minify-table-aliased.encoded.js | 25 + .../minify-table-anti.encoded.js | 27 + .../minify-table-macro.encoded.js | 24 + .../minify-table-randomized.encoded.js | 23 + .../minify-table-specialized.encoded.js | 26 + .../fixtures/runtime-layers/source.js | 2 + .../jsconfuser-vm/identify-handlers.test.js | 190 +++ .../jsconfuser-vm/partition-functions.test.js | 265 +++++ test/vm/jsconfuser-vm/read-wordcode.test.js | 337 ++++++ .../jsconfuser-vm/validate-references.test.js | 119 ++ 211 files changed, 65226 insertions(+) create mode 100644 src/vm/jsconfuser-vm/container-role-helpers.js create mode 100644 src/vm/jsconfuser-vm/extract-container.js create mode 100644 src/vm/jsconfuser-vm/handler-effects.js create mode 100644 src/vm/jsconfuser-vm/handler-matchers.js create mode 100644 src/vm/jsconfuser-vm/handler-meaning.js create mode 100644 src/vm/jsconfuser-vm/identify-handlers.js create mode 100644 src/vm/jsconfuser-vm/locate-boot-payload.js create mode 100644 src/vm/jsconfuser-vm/locate-handlers.js create mode 100644 src/vm/jsconfuser-vm/materialize-encoded-bytecode.js create mode 100644 src/vm/jsconfuser-vm/minified-runtime-container.js create mode 100644 src/vm/jsconfuser-vm/runtime-fragment-pattern.js create mode 100644 src/vm/jsconfuser-vm/runtime-source-signature.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/generation.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-169.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-170.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-171.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/outer-layers/compact.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/outer-layers/generation.json create mode 100644 test/vm/jsconfuser-vm/fixtures/outer-layers/minify.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/outer-layers/moved-declarations.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/outer-layers/object-extraction.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/outer-layers/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/outer-layers/variable-masking.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/outer-layers/vm.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/reference/encoded.debug.js create mode 100644 test/vm/jsconfuser-vm/fixtures/reference/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/reference/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/reference/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-aliased.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-specialized.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-table-shuffle.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/generation.json create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-aliased.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-randomized.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-aliased.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-anti.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-macro.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-randomized.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-specialized.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-layers/source.js create mode 100644 test/vm/jsconfuser-vm/identify-handlers.test.js create mode 100644 test/vm/jsconfuser-vm/partition-functions.test.js create mode 100644 test/vm/jsconfuser-vm/read-wordcode.test.js create mode 100644 test/vm/jsconfuser-vm/validate-references.test.js diff --git a/src/vm/jsconfuser-vm/container-role-helpers.js b/src/vm/jsconfuser-vm/container-role-helpers.js new file mode 100644 index 00000000..884b7f00 --- /dev/null +++ b/src/vm/jsconfuser-vm/container-role-helpers.js @@ -0,0 +1,271 @@ +import traverse from '@babel/traverse' +import { CANONICAL_OPCODES as EXPECTED_OPCODES } from './opcode-table.js' + +export { EXPECTED_OPCODES } + +export const EXPECTED_SENTINELS = Object.freeze({ CALL_SPREAD: 65535 }) + +export const EXPECTED_SLOTS = Object.freeze({ + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7, +}) + +export const PARSER_OPTIONS = Object.freeze({ + allowReturnOutsideFunction: true, + errorRecovery: false, + sourceType: 'script', +}) + +export class ContainerDecline extends Error { + constructor(code, message) { + super(message) + this.name = 'ContainerDecline' + this.code = code + } +} + +export function decline(code, message) { + throw new ContainerDecline(code, message) +} + +export function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) { + return value + } + Object.freeze(value) + for (const child of Object.values(value)) deepFreeze(child) + return value +} + +export function isIdentifier(node, name = null) { + return node?.type === 'Identifier' && (name === null || node.name === name) +} + +export function propertyName(node) { + if (isIdentifier(node)) return node.name + if (node?.type === 'StringLiteral') return node.value + return null +} + +export function numberValue(node) { + return node?.type === 'NumericLiteral' && Number.isFinite(node.value) + ? node.value + : null +} + +export function isWordArray(node) { + if (node?.type !== 'ArrayExpression' || node.elements.length === 0) { + return false + } + return node.elements.every((element) => { + const value = numberValue(element) + return ( + value !== null && + Number.isSafeInteger(value) && + value >= 0 && + value <= 0xffffffff + ) + }) +} + +function isWithin(path, rootNode) { + for (let current = path; current; current = current.parentPath) { + if (current.node === rootNode) return true + } + return false +} + +export function makeContext(ast) { + const paths = new Map() + let programPath + traverse(ast, { + enter(path) { + paths.set(path.node, path) + }, + Program(path) { + programPath = path + }, + }) + if (!programPath) decline('malformed-input', 'Expected a Babel Program') + + const context = { + ast, + program: programPath.node, + paths, + programPath, + bindingFor(node) { + if (!isIdentifier(node)) return null + const path = paths.get(node) + return path?.scope.getBinding(node.name) ?? null + }, + isReference(node, binding) { + if (!isIdentifier(node) || !binding) return false + const path = paths.get(node) + return ( + !!path && + path.isReferencedIdentifier() && + path.scope.getBinding(node.name) === binding + ) + }, + nodes(rootNode) { + return [...paths.entries()] + .filter(([node]) => isWithin(paths.get(node), rootNode)) + .map(([node, path]) => ({ node, path })) + }, + // A lowered table handler's SSA version (`{ssa, index}`) refers to its binding. + sameRef(node, binding) { + if (node?.ssa) return !!binding && node.ssa === binding + return context.isReference(node, binding) + }, + } + return context +} + +export function topLevelDeclarations(context) { + const declarations = [] + for (const statement of context.program.body) { + if (statement.type === 'VariableDeclaration') { + for (const declaration of statement.declarations) { + if (!isIdentifier(declaration.id)) continue + declarations.push({ + id: declaration.id, + init: declaration.init, + kind: 'variable', + name: declaration.id.name, + node: declaration, + statement, + binding: context.bindingFor(declaration.id), + }) + } + } else if (statement.type === 'FunctionDeclaration') { + if (!isIdentifier(statement.id)) continue + declarations.push({ + id: statement.id, + init: statement, + kind: 'function', + name: statement.id.name, + node: statement, + statement, + binding: context.bindingFor(statement.id), + }) + } + } + for (const declaration of declarations) { + if (declaration.kind !== 'variable' || declaration.init) continue + const write = definingWrite(context, declaration.binding) + if (!write) continue + declaration.init = write.node.right + declaration.definingWrite = write.node + } + // A function value bound once is the same role as a function declaration. + for (const declaration of declarations) { + if ( + declaration.kind === 'variable' && + declaration.init?.type === 'FunctionExpression' && + isStableDeclaration(declaration) + ) + declaration.kind = 'function' + } + return declarations +} + +// The value of a bare top-level var is its only write when that write is a +// top-level `name = value` statement and no earlier top-level statement reads +// the binding. Declarator placement is spelling; the single +// write executed before every read is what the program depends on. +function definingWrite(context, binding) { + const writes = binding?.constantViolations ?? [] + if (writes.length !== 1) return null + const write = writes[0] + const statement = write.parentPath + if ( + !write.isAssignmentExpression({ operator: '=' }) || + !isIdentifier(write.node.left) || + !statement?.isExpressionStatement() || + !statement.parentPath?.isProgram() + ) + return null + const body = context.program.body + const at = body.indexOf(statement.node) + const readFirst = binding.referencePaths.some((reference) => { + if (reference.getFunctionParent()) return false + const top = reference.find((path) => path.parentPath?.isProgram()) + return body.indexOf(top?.node) < at + }) + return readFirst ? null : write +} + +export function isStableDeclaration(declaration) { + const writes = declaration.binding?.constantViolations ?? [] + return ( + !!declaration.binding && + (declaration.binding.path.node === declaration.node || + declaration.binding.identifier === declaration.id) && + (writes.length === 0 || + (writes.length === 1 && writes[0].node === declaration.definingWrite)) + ) +} + +export function referencePaths(context, binding, rootNode = context.program) { + if (!binding) return [] + return context + .nodes(rootNode) + .filter(({ node }) => context.isReference(node, binding)) + .map(({ path }) => path) +} + +export function referenceCount(context, binding, rootNode = context.program) { + return referencePaths(context, binding, rootNode).length +} + +export function member(node, objectPredicate, property, computed = false) { + if (node?.type !== 'MemberExpression' || node.computed !== computed) { + return false + } + if ( + typeof objectPredicate === 'function' + ? !objectPredicate(node.object) + : node.object !== objectPredicate + ) { + return false + } + return propertyName(node.property) === property +} + +export function prototypeMethods(context, owner) { + const methods = [] + for (const statement of context.program.body) { + if ( + statement.type !== 'ExpressionStatement' || + statement.expression.type !== 'AssignmentExpression' + ) { + continue + } + const left = statement.expression.left + if ( + left?.type !== 'MemberExpression' || + left.computed || + left.object?.type !== 'MemberExpression' || + !member( + left.object, + (value) => context.sameRef(value, owner.binding), + 'prototype', + ) || + statement.expression.right?.type !== 'FunctionExpression' + ) { + continue + } + methods.push({ + fn: statement.expression.right, + name: propertyName(left.property), + node: statement, + }) + } + return methods +} diff --git a/src/vm/jsconfuser-vm/extract-container.js b/src/vm/jsconfuser-vm/extract-container.js new file mode 100644 index 00000000..84cae064 --- /dev/null +++ b/src/vm/jsconfuser-vm/extract-container.js @@ -0,0 +1,75 @@ +import { parse } from '@babel/parser' +import { locateBootPayload } from './locate-boot-payload.js' +import { locateHandlers } from './locate-handlers.js' +import { behaviourTable } from './identify-handlers.js' +import { + ContainerDecline, + EXPECTED_SENTINELS, + PARSER_OPTIONS, + decline, + deepFreeze, +} from './container-role-helpers.js' + +function parseInput(input) { + if (typeof input === 'string') return parse(input, PARSER_OPTIONS) + if (input?.type === 'File' || input?.type === 'Program') return input + decline( + 'invalid-input', + 'Container extractor expects source text or a Babel AST', + ) +} + +// The container: the payload located by the boot skeleton (locate-boot-payload.js), with the +// opcode table the handlers' behaviour gives when the runtime is located; otherwise the canonical +// table. +export function diagnoseContainer(input) { + try { + const ast = parseInput(input) + const located = locateBootPayload(ast) + const matched = meaningFromHandlers(ast) + if (matched) + return deepFreeze({ ok: true, result: matched, diagnostic: null }) + return deepFreeze({ ok: true, result: located, diagnostic: null }) + } catch (error) { + if (error instanceof ContainerDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: 'extractor-error', message: error.message }, + }) + } +} + +// The opcode table by handler behaviour (identify-handlers.js), published whatever the words hold: +// added opcodes are left to the readers readWordcode's retries open. None when the runtime is not +// located. +function meaningFromHandlers(ast) { + let view + try { + view = locateHandlers(ast) + } catch (error) { + if (error instanceof ContainerDecline) return null + throw error + } + const { opcodes, sentinel } = behaviourTable(view) + return locateBootPayload(ast, { + opcodes, + sentinels: { CALL_SPREAD: sentinel ?? EXPECTED_SENTINELS.CALL_SPREAD }, + }) +} + +export function extractContainer(input) { + return diagnoseContainer(input).result +} + +export function extractContainerFromSource(source) { + return extractContainer(source) +} + +export default extractContainer diff --git a/src/vm/jsconfuser-vm/handler-effects.js b/src/vm/jsconfuser-vm/handler-effects.js new file mode 100644 index 00000000..3fd3e497 --- /dev/null +++ b/src/vm/jsconfuser-vm/handler-effects.js @@ -0,0 +1,429 @@ +import { cloneRuntimeFragment } from './runtime-fragment-pattern.js' + +// What a handler does, independent of how a runtime layer spelled it. The lowered handler is +// evaluated symbolically: each operand read becomes a slot numbered in read order (`{op: k}`; a +// constant read without explicit arguments reads two), a literal stays a value, and a local is a +// carrier of its value, so whether minify kept or folded it changes nothing. A statement-level +// `a || b`, `a && b` or `c ? x : y` is the `if` it stands for. The result is the list of effects: +// writes to anything but a local, calls, conditionals, loops, throws and returns. + +export function handlerEffects(view, handler, identities = new Map()) { + const statements = handler.body.filter( + (statement) => + statement.type !== 'BreakStatement' && + statement.type !== 'EmptyStatement', + ) + const { nodes, origins } = cloneRuntimeFragment(statements) + view.lower(nodes, origins, handler) + // A binding outside the handler (the run function's state, a helper) is one identity across the + // runtime; a local version read before any write is numbered within the handler; an unbound + // name is the global it names. + const locals = new Map() + const keyOf = (node) => { + const origin = origins.get(node) + if (origin?.ssa) return origin + return view.context.bindingFor(origin ?? node) ?? `global:${node.name}` + } + const read = (node) => { + const key = keyOf(node) + if (typeof key === 'string') return { global: key.slice('global:'.length) } + if (key?.ssa) { + if (!locals.has(key)) locals.set(key, locals.size) + return { local: locals.get(key) } + } + if (!identities.has(key)) identities.set(key, identities.size) + return { free: identities.get(key) } + } + return evaluateEffects(nodes, { + keyOf, + read, + unbound: (node) => !view.context.bindingFor(origins.get(node) ?? node), + }) +} + +// The effects of a catalogue template (handler-matchers.js), already in lowered form: a `$name` +// placeholder is the role it names (`{role}`), any other name the global it names. +export function templateEffects(nodes) { + return evaluateEffects(nodes, { + keyOf: (node) => node.name, + read: (node) => + node.name.startsWith('$') + ? { role: node.name.slice(1) } + : { global: node.name }, + unbound: (node) => !node.name.startsWith('$'), + }) +} + +function evaluateEffects(nodes, { keyOf, read, unbound }) { + let slots = 0 + // Operand reads made inside array literals: an alias or anti handler reads all of its operands + // up front into one array. + let eager = 0 + let arrays = 0 + // Writes made inside an expression (`if ((x = e))`), flushed before the effect that holds them. + const pending = [] + const flush = (out) => out.push(...pending.splice(0)) + // Bitwise chains associate on int32 values, so a regrouped chain is one list of operands. + const ASSOCIATIVE = new Set(['^', '&', '|']) + // A compound assignment (`x &= y`) and a written-out chain build the same term. + const binary = (operator, left, right) => { + if (!ASSOCIATIVE.has(operator)) return { operator, left, right } + const parts = (term) => + term?.operator === operator && Array.isArray(term.operands) + ? term.operands + : [term] + return { operator, operands: [...parts(left), ...parts(right)] } + } + const value = (node, env) => { + if (!node) return null + switch (node.type) { + case 'CallExpression': { + const name = node.callee.type === 'Identifier' ? node.callee.name : null + if (name === '$operand') return { op: slots++ } + if (name === '$constant') + return node.arguments.length === 0 + ? { constant: [{ op: slots++ }, { op: slots++ }] } + : { constant: node.arguments.map((item) => value(item, env)) } + const callee = value(node.callee, env) + const args = node.arguments.map((item) => value(item, env)) + // `Array(n)` constructs as `new Array(n)` does. + if (callee?.global === 'Array') return { new: callee, args } + return { call: callee, args } + } + case 'NewExpression': + return { + new: value(node.callee, env), + args: node.arguments.map((item) => value(item, env)), + } + case 'NumericLiteral': + case 'StringLiteral': + case 'BooleanLiteral': + return { literal: node.value } + case 'NullLiteral': + return { literal: null } + case 'ThisExpression': + return { this: true } + case 'Identifier': { + if (node.name === 'undefined' && unbound(node)) + return { undefined: true } + const key = keyOf(node) + return env.has(key) ? env.get(key) : read(node) + } + case 'MemberExpression': { + const object = value(node.object, env) + const property = node.computed + ? value(node.property, env) + : { name: node.property.name } + // An operand array indexed by a literal is that element: an alias reads its permuted + // operands this way. + if (object?.array && Number.isInteger(property?.literal)) + return object.array[property.literal] ?? { undefined: true } + return { get: [object, property] } + } + case 'ArrayExpression': { + const before = slots + arrays += 1 + const array = { array: node.elements.map((item) => value(item, env)) } + arrays -= 1 + if (arrays === 0) eager += slots - before + return array + } + case 'ObjectExpression': + return { + object: node.properties.map((item) => [ + item.computed + ? value(item.key, env) + : { name: String(item.key.name ?? item.key.value) }, + value(item.value, env), + ]), + } + case 'BinaryExpression': + return binary( + node.operator, + value(node.left, env), + value(node.right, env), + ) + case 'LogicalExpression': + return { + operator: node.operator, + left: value(node.left, env), + right: value(node.right, env), + } + case 'UnaryExpression': { + // Minify's spellings of constants: `void 0` is undefined, `!0` and `!1` are booleans. + const literal = ['NumericLiteral', 'BooleanLiteral', 'StringLiteral'] + if (node.operator === 'void' && literal.includes(node.argument.type)) + return { undefined: true } + if (node.operator === '!' && literal.includes(node.argument.type)) + return { literal: !node.argument.value } + return { unary: node.operator, argument: value(node.argument, env) } + } + case 'UpdateExpression': + return { update: node.operator, argument: value(node.argument, env) } + case 'ConditionalExpression': + return { + test: value(node.test, env), + then: value(node.consequent, env), + else: value(node.alternate, env), + } + case 'TemplateLiteral': { + // The string concatenation it evaluates to, as the outer Preparation stage writes it: + // the first quasi, then each expression and each nonempty quasi after it, left to right. + let chain = { literal: node.quasis[0].value.cooked } + node.expressions.forEach((item, index) => { + chain = { operator: '+', left: chain, right: value(item, env) } + const next = node.quasis[index + 1].value.cooked + if (next !== '') + chain = { operator: '+', left: chain, right: { literal: next } } + }) + return chain + } + case 'AssignmentExpression': + return assign(node, env, pending) + case 'SequenceExpression': + return { sequence: node.expressions.map((item) => value(item, env)) } + default: + return { node: node.type } + } + } + // JavaScript evaluates an assignment's target (object, then index) before its value, so operand + // reads are numbered in that order. + const assign = (node, env, out) => { + const target = + node.left.type === 'Identifier' ? null : value(node.left, env) + const right = + node.operator === '=' + ? value(node.right, env) + : binary( + node.operator.slice(0, -1), + target ?? value(node.left, env), + value(node.right, env), + ) + if (node.left.type === 'Identifier') env.set(keyOf(node.left), right) + else { + if (out !== pending) flush(out) + out.push({ set: target, value: right }) + } + return right + } + const asIf = (test, consequent, alternate = null) => ({ + type: 'IfStatement', + test, + consequent: { type: 'ExpressionStatement', expression: consequent }, + alternate: alternate && { + type: 'ExpressionStatement', + expression: alternate, + }, + }) + // A local written inside a conditional or loop holds, after it, what the paths leave: the one + // value when every path agrees, otherwise the join of the paths' values. + const join = (env, kind, test, branches) => { + const keys = new Set(branches.flatMap((branch) => [...branch.keys()])) + for (const key of keys) { + const values = branches.map((branch) => + branch.has(key) ? branch.get(key) : (env.get(key) ?? { unset: true }), + ) + const before = env.has(key) ? JSON.stringify(env.get(key)) : null + if (values.every((item) => JSON.stringify(item) === before)) continue + env.set( + key, + values.every( + (item) => JSON.stringify(item) === JSON.stringify(values[0]), + ) + ? values[0] + : { join: kind, test, values }, + ) + } + } + const statement = (node, env, out) => { + switch (node.type) { + case 'ExpressionStatement': { + const expression = node.expression + if (expression.type === 'AssignmentExpression') + return assign(expression, env, out) + if ( + expression.type === 'LogicalExpression' && + expression.operator !== '??' + ) + return statement( + asIf( + expression.operator === '||' + ? { + type: 'UnaryExpression', + operator: '!', + argument: expression.left, + } + : expression.left, + expression.right, + ), + env, + out, + ) + if (expression.type === 'ConditionalExpression') + return statement( + asIf(expression.test, expression.consequent, expression.alternate), + env, + out, + ) + if (expression.type === 'SequenceExpression') { + for (const item of expression.expressions) + statement( + { type: 'ExpressionStatement', expression: item }, + env, + out, + ) + return + } + const done = value(expression, env) + flush(out) + out.push({ do: done }) + return + } + case 'VariableDeclaration': + for (const item of node.declarations) + if (item.init) env.set(keyOf(item.id), value(item.init, env)) + return + case 'BlockStatement': + for (const item of node.body) statement(item, env, out) + return + case 'IfStatement': { + const test = value(node.test, env) + flush(out) + const then = [] + const otherwise = [] + const thenEnv = new Map(env) + const elseEnv = new Map(env) + statement(node.consequent, thenEnv, then) + if (node.alternate) statement(node.alternate, elseEnv, otherwise) + out.push({ if: test, then, else: otherwise }) + join(env, 'if', test, [thenEnv, elseEnv]) + return + } + case 'ThrowStatement': { + const thrown = value(node.argument, env) + flush(out) + out.push({ throw: thrown }) + return + } + case 'ReturnStatement': { + const returned = value(node.argument, env) + flush(out) + out.push({ return: returned }) + return + } + case 'ForStatement': + case 'WhileStatement': + case 'DoWhileStatement': + case 'ForInStatement': + case 'ForOfStatement': { + // A for loop is its init, then a while loop whose body ends with its update. + if (node.init) + statement( + node.init.type === 'VariableDeclaration' + ? node.init + : { type: 'ExpressionStatement', expression: node.init }, + env, + out, + ) + const inner = new Map(env) + const body = [] + const test = node.test ? value(node.test, inner) : null + flush(out) + statement(node.body, inner, body) + if (node.update) + statement( + { type: 'ExpressionStatement', expression: node.update }, + inner, + body, + ) + const kind = + node.type === 'ForStatement' || node.type === 'WhileStatement' + ? 'while' + : node.type + out.push({ loop: kind, test, body }) + join(env, 'loop', test, [new Map(env), inner]) // zero or more passes + return + } + // A debugger statement acts only under a debugger: production behaviour is unchanged. + case 'DebuggerStatement': + return + default: + out.push({ statement: node.type }) + } + } + const effects = [] + const env = new Map() + for (const node of nodes) statement(node, env, effects) + return { effects, operands: slots, eager: slots > 0 && eager === slots } +} + +const isSlot = (term) => + term && + typeof term === 'object' && + 'op' in term && + Object.keys(term).length === 1 + +// Whether `actual` does what `reference` does with each reference operand slot bound to one +// actual term: a literal (a baked operand) or an actual slot (a permuted operand). The binding is +// recorded in `slots` (reference slot -> actual term). +export function unifyEffects(reference, actual, slots = new Map()) { + if (isSlot(reference)) { + if (slots.has(reference.op)) + return JSON.stringify(slots.get(reference.op)) === JSON.stringify(actual) + if ( + !isSlot(actual) && + !(actual && typeof actual === 'object' && 'literal' in actual) + ) + return false + slots.set(reference.op, actual) + return true + } + if (Array.isArray(reference)) + return ( + Array.isArray(actual) && + reference.length === actual.length && + reference.every((item, index) => unifyEffects(item, actual[index], slots)) + ) + if (reference === null || typeof reference !== 'object') + return reference === actual + if (actual === null || typeof actual !== 'object' || Array.isArray(actual)) + return false + const keys = Object.keys(reference).sort() + const actualKeys = Object.keys(actual).sort() + return ( + keys.length === actualKeys.length && + keys.every( + (key, index) => + key === actualKeys[index] && + unifyEffects(reference[key], actual[key], slots), + ) + ) +} + +// Every way to read `effects` as consecutive references, each with its slot binding. +export function decomposeEffects(references, effects, limit = 2) { + const found = [] + const walk = (start, parts) => { + if (found.length >= limit) return + if (start === effects.length) { + found.push(parts) + return + } + for (const reference of references) { + const length = reference.effects.length + if (length === 0 || start + length > effects.length) continue + const slots = new Map() + if ( + !unifyEffects( + reference.effects, + effects.slice(start, start + length), + slots, + ) + ) + continue + walk(start + length, [...parts, { reference, slots }]) + } + } + walk(0, []) + return found +} diff --git a/src/vm/jsconfuser-vm/handler-matchers.js b/src/vm/jsconfuser-vm/handler-matchers.js new file mode 100644 index 00000000..497e8386 --- /dev/null +++ b/src/vm/jsconfuser-vm/handler-matchers.js @@ -0,0 +1,204 @@ +// One reference per canonical opcode, written in the handler locator's lowered form and compared by +// effects (handler-meaning.js), so every spelling a runtime layer or an outer encoder gives the same +// operation fits it: `$operand()` and `$constant(...)` are the reader calls; `$name` is a role bound +// to one actual term, `this` or a property name; `$_ROLE` binds a number (a frame-header offset or +// the spread sentinel). Roles in DISPATCH_ROLES name one thing across the whole dispatch; the others +// are local to one handler. A helper is called as a method here; its devirtualized function form +// fits too. The one spelling effects cannot equate is a helper's body inlined into the handler +// (Closure minify's upvalue cell), so those two opcodes also keep the inlined form. + +const read = '$operand()' +const reg = `$regs[$base+${read}]` +const dst = `$dst=${read};` +const put = (value) => `$regs[$base+$dst]=${value};` + +const matchers = [] +const add = (name, ...sources) => + sources.forEach((source) => matchers.push({ name, source })) + +add('LOAD_CONST', dst + put('$constant()')) +add('LOAD_INT', dst + put(read)) +add( + 'LOAD_GLOBAL', + `${dst}$name=$constant();if(!($name in $vm.$globalsField))throw new ReferenceError(\`\${$name} is not defined\`);${put('$vm.$globalsField[$name]')}`, +) +// Minify can inline the upvalue cell's read and write into the handler. +const cell = `$cell=$regs[$fp+$_CLOSURE].$upvaluesField` +const cellSlot = '$cell.$cellRegsField[$cell.$cellSlotField]' +add( + 'LOAD_UPVALUE', + dst + put(`$regs[$fp+$_CLOSURE].$upvaluesField[${read}].$readMethod()`), + `${dst}${cell}[${read}];${put(`$cell.$closedField?$cell.$cellValueField:${cellSlot}`)}`, +) +add( + 'STORE_UPVALUE', + `$index=${read};$regs[$fp+$_CLOSURE].$upvaluesField[$index].$writeMethod(${reg});`, + `$index=${read};${cell}[$index];$value=${reg};$cell.$closedField?$cell.$cellValueField=$value:${cellSlot}=$value;`, +) +add('LOAD_THIS', dst + put('$regs[$fp+$_THIS]')) +add('MOVE', dst + put(reg)) +add('STORE_GLOBAL', `$vm.$globalsField[$constant()]=${reg};`) +add('GET_PROP', `${dst}$object=${reg};$key=${reg};${put('$object[$key]')}`) +add( + 'SET_PROP', + `$object=${reg};$key=${reg};$value=${reg};Reflect.set($object,$key,$value);`, +) +add( + 'DELETE_PROP', + `${dst}$object=${reg};$key=${reg};${put('delete $object[$key]')}`, +) +for (const [name, operator] of [ + ['ADD', '+'], + ['SUB', '-'], + ['MUL', '*'], + ['DIV', '/'], + ['MOD', '%'], + ['BAND', '&'], + ['BOR', '|'], + ['BXOR', '^'], + ['SHL', '<<'], + ['SHR', '>>'], + ['USHR', '>>>'], + ['LT', '<'], + ['GT', '>'], + ['LTE', '<='], + ['GTE', '>='], + ['EQ', '==='], + ['NEQ', '!=='], + ['LOOSE_EQ', '=='], + ['LOOSE_NEQ', '!='], + ['IN', 'in'], + ['INSTANCEOF', 'instanceof'], +]) + add(name, `${dst}$left=${reg};${put(`$left ${operator} ${reg}`)}`) +add('EXP', `${dst}$left=${reg};${put(`Math.pow($left,${reg})`)}`) +for (const [name, operator] of [ + ['UNARY_NEG', '-'], + ['UNARY_POS', '+'], + ['UNARY_NOT', '!'], + ['UNARY_BITNOT', '~'], + ['TYPEOF', 'typeof '], +]) + add(name, dst + put(`${operator}${reg}`)) +add('VOID', `${dst}${read};${put('undefined')}`) +add( + 'TYPEOF_SAFE', + `${dst}$name=$constant();$value=Object.prototype.hasOwnProperty.call($vm.$globalsField,$name)?$vm.$globalsField[$name]:undefined;${put('typeof $value')}`, +) +add('JUMP', `$regs[$fp+$_PC]=${read};`) +add( + 'JUMP_IF_FALSE', + `$condition=${read};$target=${read};if(!$regs[$base+$condition])$regs[$fp+$_PC]=$target;`, +) +add( + 'JUMP_IF_TRUE', + `$condition=${read};$target=${read};if($regs[$base+$condition])$regs[$fp+$_PC]=$target;`, +) +for (const [name, receiver, thisValue, flag, host] of [ + ['CALL', '', '$vm.$globalsField', '', '$callee.apply(null,$args)'], + [ + 'CALL_METHOD', + `$receiver=${reg};`, + '$receiver', + '', + '$callee.apply($receiver,$args)', + ], + [ + 'NEW', + '', + 'Object.create($closure.prototype||null)', + '|1', + 'Reflect.construct($callee,$args)', + ], +]) + add( + name, + `${dst}${receiver}$callee=${reg};$argc=${read};if($argc===$_SPREAD)$args=${reg};else{$args=new Array($argc);for(var $i=0;$i<$argc;$i++)$args[$i]=${reg};} + $closure=$callee&&$closureMap.get($callee);if($closure)$vm.$pushFrameMethod($closure,$args,${thisValue},$dst<<1${flag});else $regs[$base+$dst]=${host};`, + ) +add( + 'RETURN', + `$value=${reg};$vm.$closeMethod($fp);$caller=$regs[$fp+$_CALLER];$retDst=$regs[$fp+$_RET_DST]; + if($retDst&1&&(typeof $value!=="object"||$value===null))$value=$regs[$fp+$_THIS]; + $end=$fp+$regs[$fp+$_FRAME_SIZE];for(var $i=$fp;$i<$end;$i++)$regs[$i]=undefined; + $vm.$topField=$fp;$vm.$frameField=$caller;$regs[$caller?$regs[$caller+$_REG_BASE]+($retDst>>1):0]=$value;`, +) +add('THROW', `throw ${reg};`) +add( + 'MAKE_CLOSURE', + `${dst}$startPc=${read};$paramCount=${read};$regCount=${read};$uvCount=${read};$hasRest=${read}; + $descs=new Array($uvCount); + for($i=0;$i<$uvCount;$i++){$isLocal=${read};$uvIndex=${read};$descs[$i]={$localField:$isLocal,$indexField:$uvIndex};} + $fn={$paramField:$paramCount,$countField:$regCount,$startField:$startPc,$restField:$hasRest};$closure=new $Closure($fn); + for($i=0;$i<$descs.length;$i++){$desc=$descs[$i];if($desc.$localField)$closure.$upvaluesField.push($vm.$captureMethod($fp,$desc.$indexField));else $closure.$upvaluesField.push($regs[$fp+$_CLOSURE].$upvaluesField[$desc.$indexField]);} + $self=$vm;$shell=function($inner){return function(){return new $VM($self.$wordsField,$self.$poolField,$self.$globalsField).$runMethod($inner,this==null?$self.$globalsField:this,Array.prototype.slice.call(arguments));};}($closure); + $closureMap.set($shell,$closure);$shell.prototype=$closure.prototype;${put('$shell')}`, +) +add( + 'BUILD_ARRAY', + `${dst}$count=${read};$array=new Array($count);for(var $i=0;$i<$count;$i++)$array[$i]=${reg};${put('$array')}`, +) +add( + 'BUILD_OBJECT', + `${dst}$count=${read};$object={};for(var $i=0;$i<$count;$i++){$key=${reg};$value=${reg};$object[$key]=$value;}${put('$object')}`, +) +for (const [name, kind, other] of [ + ['DEFINE_GETTER', 'get', 'set'], + ['DEFINE_SETTER', 'set', 'get'], +]) + add( + name, + `$object=${reg};$key=${reg};$accessor=${reg};$existing=Object.getOwnPropertyDescriptor($object,$key);$descriptor={${kind}:$accessor,configurable:true,enumerable:true};if($existing&&typeof $existing.${other}==="function")$descriptor.${other}=$existing.${other};Object.defineProperty($object,$key,$descriptor);`, + ) +add( + 'FOR_IN_SETUP', + `${dst}$object=${reg};$keys=[];if($object!==null&&$object!==undefined){$seen=Object.create(null);$current=Object($object);while($current!==null){$own=Object.getOwnPropertyNames($current);for(var $i=0;$i<$own.length;$i++){$key=$own[$i];if(!($key in $seen)){$seen[$key]=true;$property=Object.getOwnPropertyDescriptor($current,$key);if($property&&$property.enumerable)$keys.push($key);}}$current=Object.getPrototypeOf($current);}}${put('{$keysField:$keys,$iteratorIndexField:0}')}`, +) +add( + 'FOR_IN_NEXT', + `${dst}$iterator=${reg};$target=${read};if($iterator.$iteratorIndexField>=$iterator.$keysField.length)$regs[$fp+$_PC]=$target;else $regs[$base+$dst]=$iterator.$keysField[$iterator.$iteratorIndexField++];`, +) +const handlerStack = + '$handlers=$regs[$fp+$_HANDLERS];if(!$handlers)$regs[$fp+$_HANDLERS]=$handlers=[];' +add( + 'TRY_SETUP', + `${handlerStack}$handlers.push({$handlerPcField:${read},$exceptionRegField:${read}});`, +) +add( + 'FINALLY_SETUP', + `${handlerStack}$handlers.push({$finallyPcField:${read},$contRegField:${read},$payloadRegField:${read},$throwPadField:${read}});`, +) +add('TRY_END', '$regs[$fp+$_HANDLERS].pop();') +add( + 'PATCH', + `$destPc=${read};$sliceStart=${read};$sliceEnd=${read};$key=${read}^$destPc|0;for(var $i=$sliceStart;$i<$sliceEnd;$i++){$key=$key+2654435769|0;$vm.$wordsField[$destPc+($i-$sliceStart)]=($vm.$wordsField[$i]^($key^$key>>>13))>>>0;}`, +) +add('DEBUGGER', 'debugger;') +add('JUMP_REG', `$regs[$fp+$_PC]=${reg};`) + +// Bound once for the whole dispatch: the interpreter's own state, helpers and fields. +const DISPATCH_NAMES = new Set([ + 'vm', + 'regs', + 'base', + 'fp', + 'closureMap', + 'readUpvalue', + 'writeUpvalue', + 'pushFrame', + 'closeFrame', + 'capture', + 'Closure', + 'VM', + 'run', +]) +export function isDispatchRole(name) { + return ( + name.startsWith('_') || + DISPATCH_NAMES.has(name) || + name.endsWith('Field') || + name.endsWith('Method') + ) +} + +export const HANDLER_MATCHERS = matchers diff --git a/src/vm/jsconfuser-vm/handler-meaning.js b/src/vm/jsconfuser-vm/handler-meaning.js new file mode 100644 index 00000000..b7843c12 --- /dev/null +++ b/src/vm/jsconfuser-vm/handler-meaning.js @@ -0,0 +1,250 @@ +import { parse } from '@babel/parser' +import { normalizeStatements } from './locate-handlers.js' +import { CANONICAL_OPCODES } from './opcode-table.js' +import { HANDLER_MATCHERS, isDispatchRole } from './handler-matchers.js' +import { handlerEffects, templateEffects } from './handler-effects.js' + +// Runtime meaning: an opcode is what its handler does, never its label or its spelling. A handler's +// effects (handler-effects.js) are compared with each canonical opcode's reference effects, +// evaluated from its catalogue template, and only when a stage needs its opcode. The spread +// sentinel is the value CALL compares its argument count with. + +function withoutBreaks(statements) { + return statements.filter( + (statement) => + statement.type !== 'BreakStatement' && + statement.type !== 'EmptyStatement', + ) +} + +function templateStatements(source) { + const program = parse( + `function template(){while(true){switch(0){case 0:${source}}}}`, + ).program + const statements = + program.body[0].body.body[0].body.body[0].cases[0].consequent + return withoutBreaks(normalizeStatements(statements)) +} + +const REFERENCES = HANDLER_MATCHERS.map(({ name, source }) => ({ + name, + ...templateEffects(templateStatements(source)), +})) + +// Fitting binds `$name` roles. Dispatch roles live in `roles.dispatch` for every handler, and no +// two of them share a value; the others live in `roles.local` for this handler only, and may share +// one, since minify keeps several of a handler's values in one variable. +function bind(roles, name, value) { + const dispatch = isDispatchRole(name) + const table = dispatch ? roles.dispatch : roles.local + if (table.has(name)) return table.get(name) === value + if (dispatch) + for (const bound of table.values()) if (bound === value) return false + table.set(name, value) + return true +} + +const isObject = (term) => + term !== null && typeof term === 'object' && !Array.isArray(term) +const isPropertyRole = (term) => + isObject(term) && + Object.keys(term).length === 1 && + typeof term.name === 'string' && + term.name.startsWith('$') +const isRole = (term) => + isObject(term) && Object.keys(term).length === 1 && 'role' in term + +// A call to an interpreter helper or constructor (`$pushFrame(...)`, `$vm.$pushFrameMethod(...)`, +// `new $Closure(...)`). Class obfuscation turns a VM method into a function taking the VM first and +// permutes helper parameters, so a helper call is its callee and an unordered list of arguments, +// a method call's receiver among them. +function helperCall(term, reference) { + if (!isObject(term)) return null + const kind = 'call' in term ? 'call' : 'new' in term ? 'new' : null + if (!kind || Object.keys(term).length !== 2) return null + const callee = term[kind] + const method = + kind === 'call' && + isObject(callee) && + Array.isArray(callee.get) && + isObject(callee.get[1]) && + typeof callee.get[1].name === 'string' + if (reference) { + const helper = method + ? isPropertyRole(callee.get[1]) + : isRole(callee) && + isDispatchRole(callee.role) && + !callee.role.startsWith('_') + if (!helper) return null + } + return method + ? { kind, callee: callee.get[1], args: [callee.get[0], ...term.args] } + : { kind, callee, args: term.args } +} + +// Arguments left over must be dummy literals: minify can leave a decoy argument whose field the +// decoy test cannot single out. +const isDummy = (term) => + isObject(term) && + (('literal' in term && Object.keys(term).length === 1) || + term.undefined === true || + (Array.isArray(term.array) && term.array.length === 0) || + (Array.isArray(term.object) && term.object.length === 0)) + +// Whether the actual effects do what the reference effects do: the same terms, each operand slot +// in the same position, each role bound to one actual term. +function fit(reference, actual, roles) { + if (Array.isArray(reference)) + return ( + Array.isArray(actual) && + reference.length === actual.length && + reference.every((item, index) => fit(item, actual[index], roles)) + ) + if (!isObject(reference)) return reference === actual + if (!isObject(actual)) return false + if (isRole(reference)) { + const name = reference.role + // `$_ROLE` is a number: a frame-header offset or the spread sentinel. + if (name.startsWith('_')) + return ( + Object.keys(actual).length === 1 && + typeof actual.literal === 'number' && + bind(roles, name, actual.literal) + ) + return bind(roles, name, JSON.stringify(actual)) + } + if (isPropertyRole(reference)) + return ( + Object.keys(actual).length === 1 && + typeof actual.name === 'string' && + bind(roles, reference.name.slice(1), `property:${actual.name}`) + ) + const helper = helperCall(reference, true) + if (helper) { + const call = helperCall(actual, false) + return ( + Boolean(call) && + call.kind === helper.kind && + fitCallee(helper.callee, call.callee, roles) && + unordered(helper.args, call.args, roles) + ) + } + const keys = Object.keys(reference).sort() + const actualKeys = Object.keys(actual).sort() + return ( + keys.length === actualKeys.length && + keys.every( + (key, index) => + key === actualKeys[index] && fit(reference[key], actual[key], roles), + ) + ) +} + +// A helper's role names whatever the runtime calls: a method's property name, or the function +// class obfuscation made of it. +function fitCallee(reference, actual, roles) { + if (!isPropertyRole(reference) || !isObject(actual)) + return fit(reference, actual, roles) + const value = + Object.keys(actual).length === 1 && typeof actual.name === 'string' + ? `property:${actual.name}` + : JSON.stringify(actual) + return bind(roles, reference.name.slice(1), value) +} + +function unordered(references, actuals, roles) { + if (references.length > actuals.length) return false + const assign = (index, used) => { + if (index === references.length) + return actuals.every( + (term, position) => used.has(position) || isDummy(term), + ) + for (let candidate = 0; candidate < actuals.length; candidate++) { + if (used.has(candidate)) continue + const trial = { + dispatch: new Map(roles.dispatch), + local: new Map(roles.local), + } + if (!fit(references[index], actuals[candidate], trial)) continue + used.add(candidate) + if (assign(index + 1, used)) { + roles.dispatch = trial.dispatch + roles.local = trial.local + return true + } + used.delete(candidate) + } + return false + } + return assign(0, new Set()) +} + +// The meaning of a handler is found when a stage needs it. `opcodeFor(label)` names the opcode +// whose references fit the handler with that label, provided exactly one opcode fits it and that +// opcode's references fit no other handler; otherwise null (the decode's default stays). Matches +// bind the dispatch roles they commit to for every later question. +export function meaningResolver(view) { + // One identity per binding across the runtime, so a dispatch role binds the same term in every + // handler. + const identities = new Map() + const bodies = new Map( + view.handlers.map((handler) => [ + handler.label, + { handler, ...handlerEffects(view, handler, identities) }, + ]), + ) + // The locator already knows the payload fields from the VM construction. + const dispatch = new Map([ + ['wordsField', `property:${view.fields.words}`], + ['poolField', `property:${view.fields.pool}`], + ]) + // Dispatch roles only grow and every role constrains a match, so a template that failed to fit + // a handler still fails later; such misses are remembered. + const misses = new Set() + const fits = (name, body) => { + const key = `${name}:${body.handler.label}` + if (misses.has(key)) return null + const roles = fitsNow(name, body) + if (!roles) misses.add(key) + return roles + } + const fitsNow = (name, body) => { + for (const reference of REFERENCES) { + if (reference.name !== name || reference.operands !== body.operands) + continue + const roles = { dispatch: new Map(dispatch), local: new Map() } + if (fit(reference.effects, body.effects, roles)) return roles + } + return null + } + const answers = new Map() + // The one opcode whose template fits the handler at `label` and no other handler of `among`. + const nameAmong = (label, among) => { + const body = bodies.get(label) + if (!body) return null + const names = Object.keys(CANONICAL_OPCODES).flatMap((name) => { + const roles = fits(name, body) + return roles ? [{ name, roles }] : [] + }) + if ( + names.length !== 1 || + among.some((other) => { + const candidate = bodies.get(other) + return candidate !== body && fits(names[0].name, candidate) + }) + ) + return null + for (const [key, value] of names[0].roles.dispatch) dispatch.set(key, value) + return names[0].name + } + const opcodeFor = (label) => { + if (!answers.has(label)) + answers.set(label, nameAmong(label, [...bodies.keys()])) + return answers.get(label) + } + return { + opcodeFor, + nameAmong, + sentinel: () => dispatch.get('_SPREAD') ?? null, + } +} diff --git a/src/vm/jsconfuser-vm/identify-handlers.js b/src/vm/jsconfuser-vm/identify-handlers.js new file mode 100644 index 00000000..c5a934d3 --- /dev/null +++ b/src/vm/jsconfuser-vm/identify-handlers.js @@ -0,0 +1,188 @@ +import { requireHandlers } from './locate-handlers.js' +import { meaningResolver } from './handler-meaning.js' +import { + decomposeEffects, + handlerEffects, + unifyEffects, +} from './handler-effects.js' +import { CANONICAL_OPCODES, createOpcodeTable } from './opcode-table.js' + +// The canonical handlers by behaviour, all at once. A handler the behaviour matchers name +// uniquely is that opcode. An encoder's added handlers are built from canonical bodies, so a +// matcher can fit an original handler and its copies alike and name none of them: each such +// handler is then asked again among the named ones only, and of the handlers one template fits, the +// one reading the most operands is the original (a copy bakes some of them in). Handlers with +// equal effects (an alias in identity order) share one meaning; the lowest label stands for it. +// The rest are added handlers, for the added-opcode readers. +export function behaviourTable(view) { + const resolver = meaningResolver(view) + const named = new Map() + for (const handler of view.handlers) { + const name = resolver.opcodeFor(handler.label) + if (name) named.set(handler, name) + } + const taken = new Set(named.values()) + const namedLabels = [...named.keys()].map((handler) => handler.label) + const groups = new Map() + for (const handler of view.handlers) { + if (named.has(handler)) continue + const name = resolver.nameAmong(handler.label, namedLabels) + if (!name || taken.has(name)) continue + groups.set(name, [...(groups.get(name) ?? []), handler]) + } + const identities = new Map() + for (const [name, members] of groups) { + const read = members.map((handler) => ({ + handler, + ...handlerEffects(view, handler, identities), + })) + const most = Math.max(...read.map((item) => item.operands)) + const originals = read + .filter((item) => item.operands === most) + .sort((left, right) => left.handler.label - right.handler.label) + const [first, ...rest] = originals + if (rest.every((item) => sameEffects(first.effects, item.effects))) + named.set(first.handler, name) + } + + // Values come from labels. A name no handler answers keeps its canonical default unless an + // identified handler already holds that value. + const opcodes = createOpcodeTable() + const labels = new Set([...named.keys()].map((handler) => handler.label)) + for (const name of Object.keys(CANONICAL_OPCODES)) + if (labels.has(opcodes[name])) opcodes[name] = null + for (const [handler, name] of named) opcodes[name] = handler.label + return { opcodes, sentinel: resolver.sentinel(), named } +} + +// Equal effects: the same terms, each operand slot in the same position. +function sameEffects(left, right) { + return JSON.stringify(left) === JSON.stringify(right) +} + +// The handler view with this decode's opcode table by behaviour and its added handlers classified +// (`added`: label -> class), for the added-opcode readers. +export function requireIdentifiedHandlers(ast, code) { + const view = requireHandlers(ast, code) + const table = behaviourTable(view) + return { ...view, opcodes: table.opcodes, added: classifyAdded(view, table) } +} + +// Operands whose count depends on the instruction: a specialized handler cannot bake them. +const VARIABLE_WIDTH = new Set([ + 'MAKE_CLOSURE', + 'BUILD_ARRAY', + 'BUILD_OBJECT', + 'CALL', + 'CALL_METHOD', + 'NEW', +]) + +const isSlot = (term) => term && typeof term === 'object' && 'op' in term +const isLiteral = (term) => + term && typeof term === 'object' && 'literal' in term + +// Each added handler by what it does, against the canonical handlers `table` names: +// - specialized: one canonical handler with every operand slot a literal (its baked operands); +// - alias: one canonical handler whose slots are its own reads in another order +// (`canonicalToStored[j]` is the read that canonical operand j takes); +// - macro: two or more canonical handlers in sequence, operands read as they are used; +// - anti: two or more canonical handlers in sequence, all operands read up front into one array. +// A handler that fits none, or more than one way, is left unclassified (null). +export function classifyAdded(view, table) { + const identities = new Map() + const references = [...table.named].map(([handler, name]) => ({ + name, + opcode: handler.label, + ...handlerEffects(view, handler, identities), + })) + const classes = new Map() + for (const handler of view.handlers) { + if (table.named.has(handler)) continue + const actual = handlerEffects(view, handler, identities) + const singles = references.flatMap((reference) => { + const slots = new Map() + return unifyEffects(reference.effects, actual.effects, slots) + ? [{ reference, slots }] + : [] + }) + classes.set(handler.label, classify(singles, actual, references)) + } + return classes +} + +function classify(singles, actual, references) { + if (singles.length > 1) return null + if (singles.length === 1) { + const [{ reference, slots }] = singles + const bound = [...Array(reference.operands).keys()].map((index) => + slots.get(index), + ) + if ( + bound.length > 0 && + bound.every(isLiteral) && + !VARIABLE_WIDTH.has(reference.name) + ) + return { + kind: 'specialized', + name: reference.name, + opcode: reference.opcode, + operands: bound.map((term) => term.literal), + } + if (!bound.some(isLiteral)) fillDeadReads([bound], actual.operands) + const order = bound.map((term) => (isSlot(term) ? term.op : null)) + if ( + bound.length > 0 && + bound.length === actual.operands && + order.every((item) => item !== null) && + new Set(order).size === order.length + ) + return { + kind: 'alias', + name: reference.name, + opcode: reference.opcode, + canonicalToStored: order, + } + return null + } + const ways = decomposeEffects(references, actual.effects) + if (ways.length !== 1 || ways[0].length < 2) return null + const steps = ways[0].map(({ reference, slots }) => ({ + name: reference.name, + opcode: reference.opcode, + operands: [...Array(reference.operands).keys()].map((index) => + slots.get(index), + ), + })) + fillDeadReads( + steps.map((step) => step.operands), + actual.operands, + ) + const reads = steps.flatMap((step) => step.operands) + if (!reads.every(isSlot)) return null + const order = reads.map((term) => term.op) + if (new Set(order).size !== order.length || order.length !== actual.operands) + return null + return { + kind: actual.eager ? 'anti' : 'macro', + steps: steps.map((step) => ({ + name: step.name, + opcode: step.opcode, + reads: step.operands.map((term) => term.op), + })), + } +} + +// A read whose value no effect uses (a register the handler loads and never touches) still consumes +// a word of the stream, so a reference slot no effect binds takes the next actual read no effect +// binds either, in order. Only fresh reads can fill it; a baked literal stays unknown. +function fillDeadReads(groups, count) { + const used = new Set( + groups.flat().flatMap((term) => (isSlot(term) ? [term.op] : [])), + ) + const spare = [...Array(count).keys()].filter((read) => !used.has(read)) + for (const group of groups) + for (const [index, term] of group.entries()) + if (term === undefined && spare.length > 0) + group[index] = { op: spare.shift() } +} diff --git a/src/vm/jsconfuser-vm/locate-boot-payload.js b/src/vm/jsconfuser-vm/locate-boot-payload.js new file mode 100644 index 00000000..7bfe1cc3 --- /dev/null +++ b/src/vm/jsconfuser-vm/locate-boot-payload.js @@ -0,0 +1,267 @@ +import traverse from '@babel/traverse' +import { + EXPECTED_SENTINELS, + EXPECTED_SLOTS, + decline, + isWordArray, +} from './container-role-helpers.js' +import { decodeCanonicalBytecodeWords } from './materialize-encoded-bytecode.js' +import { createOpcodeTable } from './opcode-table.js' +import { minifiedRuntimeContainer } from './minified-runtime-container.js' + +// The boot is located by the skeleton the program cannot run without, never by names: the start +// call receives `new Closure(descriptor)` together with the VM instance, and the VM construction's +// arguments carry the payload. Every step reads values through stable top-level bindings, so +// renaming, devirtualization, inlining and argument decoys leave it intact (extract-container.md). + +const REACH_DEPTH = 8 +const HEADER_SIZE = 8 +const FRAME_START = 1 + +function programScope(ast) { + let scope = null + traverse(ast, { + Program(path) { + scope = path.scope + path.stop() + }, + }) + return scope +} + +// The value a top-level binding always holds: its initializer, or for a bare declaration its only +// write when that write is a top-level `name = value` statement. +function stableValue(scope, node) { + if (node?.type !== 'Identifier') return null + const binding = scope.getOwnBinding(node.name) + if (!binding) return null + if (binding.path.isVariableDeclarator()) { + const init = binding.path.node.init + if (init && binding.constantViolations.length === 0) return init + if (!init && binding.constantViolations.length === 1) { + const write = binding.constantViolations[0] + if ( + write.isAssignmentExpression({ operator: '=' }) && + write.parentPath.isExpressionStatement() && + write.parentPath.parentPath.isProgram() + ) + return write.node.right + } + } + return null +} + +function resolve(scope, node) { + const seen = new Set() + while (node?.type === 'Identifier' && !seen.has(node.name)) { + seen.add(node.name) + const value = stableValue(scope, node) + if (!value) return node + node = value + } + return node +} + +// A constant literal's value: plain literals, `void `, `!`, `-`, and the +// unbound `undefined`. Anything else is not a literal value. +const NOT_LITERAL = Symbol('not a literal') +function literalValue(scope, node) { + switch (node?.type) { + case 'StringLiteral': + case 'NumericLiteral': + case 'BooleanLiteral': + return node.value + case 'NullLiteral': + return null + case 'Identifier': + return node.name === 'undefined' && !scope.getBinding('undefined') + ? undefined + : NOT_LITERAL + case 'UnaryExpression': { + const argument = literalValue(scope, node.argument) + if (argument === NOT_LITERAL || typeof argument === 'symbol') + return NOT_LITERAL + if (node.operator === 'void') return undefined + if (node.operator === '!' && node.argument.type === 'NumericLiteral') + return !argument + if (node.operator === '-' && node.argument.type === 'NumericLiteral') + return -argument + return NOT_LITERAL + } + default: + return NOT_LITERAL + } +} + +function poolValues(scope, node) { + if (node?.type !== 'ArrayExpression' || node.elements.length === 0) + return null + const values = node.elements.map((element) => literalValue(scope, element)) + if (values.some((value) => value === NOT_LITERAL)) return null + return values.some((value) => typeof value === 'string') ? values : null +} + +// The payload literals an expression's value is built from: follow stable bindings and the +// arguments of calls and constructions, never a callee. +function reach(scope, node, found, depth = 0, seen = new Set()) { + if (!node || depth > REACH_DEPTH) return found + switch (node.type) { + case 'Identifier': { + if (seen.has(node.name)) return found + seen.add(node.name) + return reach(scope, stableValue(scope, node), found, depth + 1, seen) + } + case 'ArrayExpression': + if (isWordArray(node)) + found.words.push(node.elements.map((element) => element.value)) + else { + const pool = poolValues(scope, node) + if (pool) found.pools.push(pool) + } + return found + case 'StringLiteral': + try { + found.words.push(decodeCanonicalBytecodeWords(node.value)) + } catch { + // a string that is not a canonical u32 stream is not a payload + } + return found + case 'CallExpression': + case 'NewExpression': + for (const argument of node.arguments) + reach(scope, argument, found, depth + 1, seen) + return found + case 'MemberExpression': + return reach(scope, node.object, found, depth + 1, seen) + case 'BinaryExpression': + case 'LogicalExpression': + reach(scope, node.left, found, depth + 1, seen) + return reach(scope, node.right, found, depth + 1, seen) + case 'ConditionalExpression': + reach(scope, node.consequent, found, depth + 1, seen) + return reach(scope, node.alternate, found, depth + 1, seen) + case 'SequenceExpression': + return reach(scope, node.expressions.at(-1), found, depth + 1, seen) + default: + return found + } +} + +// The root descriptor: three properties whose values are {0, 0, N}; startPc and paramCount are 0. +function descriptorCount(scope, node) { + if (node?.type !== 'ObjectExpression' || node.properties.length !== 3) + return null + const values = [] + for (const property of node.properties) { + if (property.type !== 'ObjectProperty' || property.computed) return null + const value = literalValue(scope, resolve(scope, property.value)) + if (typeof value !== 'number') return null + values.push(value) + } + values.sort((left, right) => left - right) + return values[0] === 0 && + values[1] === 0 && + Number.isSafeInteger(values[2]) && + values[2] > 0 + ? values[2] + : null +} + +function sameValues(left, right) { + return ( + left.length === right.length && + left.every((value, index) => Object.is(value, right[index])) + ) +} + +function locateBoots(ast, scope) { + const boots = [] + traverse(ast, { + NewExpression(path) { + const regCount = path.node.arguments + .map((argument) => descriptorCount(scope, resolve(scope, argument))) + .find((count) => count !== null) + if (regCount === undefined) return + const call = path.parentPath + if (!call.isCallExpression() || !call.node.arguments.includes(path.node)) + return + const holders = [ + call.node.callee.type === 'MemberExpression' + ? call.node.callee.object + : null, + ...call.node.arguments, + ] + .filter(Boolean) + .map((node) => resolve(scope, node)) + .filter((node) => node.type === 'NewExpression' && node !== path.node) + for (const vm of holders) { + const found = reach(scope, vm, { words: [], pools: [] }) + if (found.words.length === 1 && found.pools.length === 1) + boots.push({ + words: found.words[0], + pool: found.pools[0], + regCount, + call: call.node, + holder: vm, + }) + } + }, + }) + return boots +} + +// The boot call and the VM construction it receives, with the construction's argument positions +// that carry the word stream and the pool: where the runtime readers enter the interpreter. +export function locateBootSkeleton(ast) { + const scope = programScope(ast) + if (!scope) decline('missing-role', 'Input has no program') + const boots = locateBoots(ast, scope) + if (boots.length !== 1) + decline('missing-role', 'Boot call is missing or not unique') + const [{ call, holder }] = boots + const carriers = (key) => + holder.arguments.flatMap((argument, index) => + reach(scope, argument, { words: [], pools: [] })[key].length === 1 + ? [index] + : [], + ) + const words = carriers('words') + const pools = carriers('pools') + if (words.length !== 1 || pools.length !== 1) + decline('missing-role', 'VM construction payload arguments are not unique') + return { call, holder, wordsIndex: words[0], poolIndex: pools[0] } +} + +// The words keep their values; with a runtime meaning (handler-meaning.js) the decode's opcode table +// and sentinel are the matched ones, otherwise the canonical ones. +export function locateBootPayload(ast, meaning = null) { + const scope = programScope(ast) + if (!scope) decline('missing-role', 'Input has no program') + const boots = locateBoots(ast, scope) + if (boots.length === 0) + decline( + 'missing-role', + 'No boot call carries a payload and root descriptor', + ) + const [boot] = boots + if ( + boots.some( + (other) => + !sameValues(other.words, boot.words) || + !sameValues(other.pool, boot.pool) || + other.regCount !== boot.regCount, + ) + ) + decline('ambiguous-role', 'Boot payload is not unique') + return minifiedRuntimeContainer({ + words: boot.words, + pool: boot.pool, + opcodes: meaning + ? { ...createOpcodeTable(), ...meaning.opcodes } + : createOpcodeTable(), + sentinels: meaning ? meaning.sentinels : EXPECTED_SENTINELS, + slots: EXPECTED_SLOTS, + root: { startPc: 0, regCount: boot.regCount }, + frame: { headerSize: HEADER_SIZE, frameStart: FRAME_START }, + }) +} diff --git a/src/vm/jsconfuser-vm/locate-handlers.js b/src/vm/jsconfuser-vm/locate-handlers.js new file mode 100644 index 00000000..ea686037 --- /dev/null +++ b/src/vm/jsconfuser-vm/locate-handlers.js @@ -0,0 +1,1046 @@ +import traverse from '@babel/traverse' +import * as t from '@babel/types' +import { locateBootSkeleton } from './locate-boot-payload.js' +import { cloneRuntimeFragment } from './runtime-fragment-pattern.js' +import { createOpcodeTable } from './opcode-table.js' +import { + isControlBodyKey, + simpleControlBody, +} from '../../visitor/jsconfuser-vm/normalize-source.js' +import { + ContainerDecline, + decline, + deepFreeze, + isStableDeclaration, + makeContext, + numberValue, + propertyName, + prototypeMethods, + topLevelDeclarations, +} from './container-role-helpers.js' + +// The handler locator finds the interpreter by what it does, never by names: the boot call enters +// the run loop with the VM its construction filled from the payload; the loop fetches the word at +// the frame's PC from the VM's word field and dispatches on it. Operand and constant readers are +// the functions whose bodies read that field and the pool field. Class obfuscation, minify +// devirtualization and case shuffle re-spell these pieces and keep the behaviour. + +function isDummyLiteral(node) { + return ( + (node?.type === 'UnaryExpression' && + node.operator === 'void' && + node.argument.type === 'NumericLiteral') || + node?.type === 'NullLiteral' || + node?.type === 'NumericLiteral' || + node?.type === 'StringLiteral' || + (node?.type === 'Identifier' && node.name === 'undefined') || + (node?.type === 'ArrayExpression' && node.elements.length === 0) || + (node?.type === 'ObjectExpression' && node.properties.length === 0) + ) +} + +function within(path, ancestor) { + for (let current = path; current; current = current.parentPath) { + if (current.node === ancestor) return true + } + return false +} + +function fakeExpression(node, parameters, context) { + if (node?.type === 'Identifier') + return parameters.has(context.bindingFor(node)) + if (node?.type !== 'ArrayExpression' && node?.type !== 'LogicalExpression') + return isDummyLiteral(node) + if (node?.type === 'ArrayExpression') + return node.elements.every((item) => + fakeExpression(item, parameters, context), + ) + if (node?.type === 'LogicalExpression') + return ( + ['&&', '||'].includes(node.operator) && + fakeExpression(node.left, parameters, context) && + fakeExpression(node.right, parameters, context) + ) + return false +} + +function functionOf(declaration) { + return declaration.init?.type === 'FunctionDeclaration' || + declaration.init?.type === 'FunctionExpression' + ? declaration.init + : null +} + +// The function a top-level binding stably holds, through stable aliases such as `var a = b`. +function functionFor(context, declarations, node, seen = new Set()) { + const binding = context.bindingFor(node) + if (!binding || seen.has(binding)) return null + seen.add(binding) + const found = declarations.filter((item) => item.binding === binding) + if (found.length === 1 && isStableDeclaration(found[0])) { + if (found[0].kind === 'function') return found[0] + if (found[0].init?.type === 'Identifier') + return functionFor(context, declarations, found[0].init, seen) + return null + } + // A declarator outside the top-level statements (a for-loop initializer) that is never written. + const alias = binding.path.isVariableDeclarator() + ? binding.path.node.init + : null + return found.length === 0 && + binding.constantViolations.length === 0 && + alias?.type === 'Identifier' + ? functionFor(context, declarations, alias, seen) + : null +} + +// Class obfuscation adds decoy parameters and stores them into a new field nothing reads; its call +// sites pass dummy literals for them. Minify then moves the store and may reuse the parameter as a +// local. The store is removed first: a write of parameters or dummy literals into the receiver's +// field (`this` in a method, the first parameter in a devirtualized function) that no code reads. +// A parameter is then dead when nothing reads it, or when its first use overwrites it. +function callableLayout(context, fn, isSelf, readFields) { + if (!fn.params.every((param) => param.type === 'Identifier')) return null + const bindings = fn.params.map((param) => context.bindingFor(param)) + const parameters = new Set(bindings) + const decoyWrites = context + .nodes(fn) + .map(({ node }) => node) + .filter( + (node) => + node.type === 'AssignmentExpression' && + node.operator === '=' && + node.left?.type === 'MemberExpression' && + !node.left.computed && + isSelf(node.left.object) && + fakeExpression(node.right, parameters, context) && + !readFields.has(propertyName(node.left.property)), + ) + const decoys = new Set() + for (const [index, binding] of bindings.entries()) { + // A use reads the parameter (a compound assignment reads its target too) or overwrites it. + const overwrites = (path) => + path.parentPath.isAssignmentExpression({ operator: '=' }) && + path.key === 'left' && + context.bindingFor(path.node) === binding + const uses = context + .nodes(fn) + .filter( + ({ node, path }) => + node.type === 'Identifier' && + (context.isReference(node, binding) || + (path.parentPath.isAssignmentExpression() && + path.key === 'left' && + context.bindingFor(node) === binding)) && + !decoyWrites.some((write) => within(path, write.right)), + ) + .sort((left, right) => left.node.start - right.node.start) + const first = uses[0]?.path + if ( + !first || + (overwrites(first) && + !context + .nodes(first.parent.right) + .some(({ node }) => context.isReference(node, binding))) + ) + decoys.add(index) + } + // Arguments past the parameters are never read unless the body uses `arguments`. + const readsArguments = context + .nodes(fn) + .some( + ({ node, path }) => + node.type === 'Identifier' && + node.name === 'arguments' && + path.getFunctionParent()?.node === fn, + ) + return { + arity: bindings.length, + decoys, + decoyWrites, + bindings, + readsArguments, + } +} + +// The number a value always holds: a literal, a top-level binding stably holding one (an +// object-extracted map member, `var x;` with its single top-level `x = 7`), or a stable numeric +// map's member (`OP.ADD`). Each spells the same case label or frame offset. +function labelValue(context, declarations, test) { + const literal = numberValue(test) + if (Number.isSafeInteger(literal)) return literal + if (test?.type === 'Identifier') { + const binding = context.bindingFor(test) + const held = declarations.find( + (item) => + item.kind === 'variable' && + binding && + item.binding === binding && + isStableDeclaration(item), + ) + const value = numberValue(held?.init) + return Number.isSafeInteger(value) ? value : null + } + if (test?.type !== 'MemberExpression' || test.computed) return null + const binding = context.bindingFor(test.object) + const map = declarations.find( + (item) => + item.kind === 'variable' && + item.binding === binding && + isStableDeclaration(item) && + item.init?.type === 'ObjectExpression', + ) + if (!map) return null + const key = propertyName(test.property) + const property = map.init.properties.find( + (item) => + item.type === 'ObjectProperty' && + !item.computed && + propertyName(item.key) === key, + ) + const value = numberValue(property?.value) + return Number.isSafeInteger(value) ? value : null +} + +function caseStatements(entry) { + return entry.consequent.length === 1 && + entry.consequent[0].type === 'BlockStatement' + ? entry.consequent[0].body + : entry.consequent +} + +// `this.field = parameter` statements directly in a constructor body, by parameter position. +function storedFields(context, fn) { + const fields = new Map() + for (const statement of fn.body.body) { + const node = statement.expression + if ( + statement.type !== 'ExpressionStatement' || + node?.type !== 'AssignmentExpression' || + node.operator !== '=' || + node.left.type !== 'MemberExpression' || + node.left.computed || + node.left.object.type !== 'ThisExpression' + ) + continue + const binding = context.bindingFor(node.right) + const index = fn.params.findIndex( + (param) => context.bindingFor(param) === binding, + ) + if (binding && index >= 0) + fields.set(index, propertyName(node.left.property)) + } + return fields +} + +function locate(ast) { + const context = makeContext(ast) + const declarations = topLevelDeclarations(context) + const { call, holder, wordsIndex, poolIndex } = locateBootSkeleton(ast) + const vm = functionFor(context, declarations, holder.callee) + const vmFunction = vm && functionOf(vm) + if (!vmFunction) decline('handler-locator', 'VM construction is not unique') + const fields = storedFields(context, vmFunction) + const wordsField = fields.get(wordsIndex) + const poolField = fields.get(poolIndex) + if (!wordsField || !poolField) + decline('handler-locator', 'VM payload fields are not stored') + + // Every method (by unique name) and top-level function with its decoy layout. + const isThis = (node) => node?.type === 'ThisExpression' + const owners = declarations.filter( + (item) => + item.kind === 'function' && prototypeMethods(context, item).length > 0, + ) + const methodCounts = new Map() + for (const owner of owners) + for (const { name } of prototypeMethods(context, owner)) + methodCounts.set(name, (methodCounts.get(name) ?? 0) + 1) + // Field names some code reads: a member access other than a plain write's target. + const readFields = new Set( + context + .nodes(context.program) + .flatMap(({ node, path }) => + node.type === 'MemberExpression' && + (!node.computed || node.property.type === 'StringLiteral') && + !( + path.parentPath.isAssignmentExpression({ operator: '=' }) && + path.key === 'left' + ) + ? [propertyName(node.property)] + : [], + ), + ) + const callables = [] + for (const owner of owners) + for (const { name, fn } of prototypeMethods(context, owner)) { + if (methodCounts.get(name) !== 1) continue + const layout = callableLayout(context, fn, isThis, readFields) + if (layout) callables.push({ kind: 'method', name, fn, owner, ...layout }) + } + for (const item of declarations) { + const fn = item.kind === 'function' ? functionOf(item) : null + if (!fn) continue + const self = fn.params[0] + // A devirtualized method writes through its first parameter; a constructor through `this`. + const isSelf = (node) => + isThis(node) || + (self?.type === 'Identifier' && + context.isReference(node, context.bindingFor(self))) + const layout = callableLayout(context, fn, isSelf, readFields) + if (layout) + callables.push({ kind: 'function', binding: item.binding, fn, ...layout }) + } + const calleeOf = (node) => { + if (node?.type !== 'CallExpression') return null + const callee = node.callee + // A method call on the VM: `this.m()` in a method, `vm.m()` in a devirtualized function. + if ( + callee.type === 'MemberExpression' && + !callee.computed && + isState(callee.object) + ) { + const name = propertyName(callee.property) + return ( + callables.find( + (item) => item.kind === 'method' && item.name === name, + ) ?? null + ) + } + if (callee.type === 'Identifier') { + const binding = context.bindingFor(callee) + return ( + callables.find( + (item) => item.kind === 'function' && item.binding === binding, + ) ?? null + ) + } + return null + } + // A method call on any receiver (an upvalue cell's read, say), by the method's unique name. + const methodOf = (node) => + node?.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + !node.callee.computed + ? (callables.find( + (item) => + item.kind === 'method' && + item.name === propertyName(node.callee.property), + ) ?? null) + : null + // A call's arguments with decoys removed. Minify drops trailing arguments, and a missing + // argument is undefined, so a call may pass fewer than the callable's arity. + // A decoy argument is dead; it need only evaluate without effect: a dummy literal, or a bound + // variable (Closure may pass one). An unbound name would throw. + const deadArgument = (node) => + isDummyLiteral(node) || + (node?.type === 'Identifier' && context.bindingFor(node) !== null) + const realArguments = (node, callable) => { + const extra = callable.readsArguments + ? [] + : node.arguments.slice(callable.arity) + if (extra.length > 0 && extra.every(deadArgument)) + return realArguments( + { ...node, arguments: node.arguments.slice(0, callable.arity) }, + callable, + ) + if (callable.decoys.size === 0 || node.arguments.length === 0) + return node.arguments + if ( + node.arguments.length > callable.arity || + [...callable.decoys].some( + (index) => + index < node.arguments.length && !deadArgument(node.arguments[index]), + ) + ) + decline('handler-locator', 'Class decoy call arguments changed') + return node.arguments.filter((_, index) => !callable.decoys.has(index)) + } + + // The run entry and how its body refers to the VM: `this` in a method, a parameter otherwise. + let entry + let stateBinding = null + if (call.callee.type === 'MemberExpression' && !call.callee.computed) { + const methods = prototypeMethods(context, vm).filter( + (item) => item.name === propertyName(call.callee.property), + ) + if (methods.length !== 1) + decline('handler-locator', 'VM run method is not unique') + entry = methods[0].fn + } else { + const run = functionFor(context, declarations, call.callee) + entry = run && functionOf(run) + const index = call.arguments.findIndex( + (argument) => + argument === holder || + (argument.type === 'Identifier' && + declarations.some( + (item) => + item.binding === context.bindingFor(argument) && + item.init === holder, + )), + ) + if (!entry || index < 0 || entry.params[index]?.type !== 'Identifier') + decline('handler-locator', 'VM run function is not unique') + stateBinding = context.bindingFor(entry.params[index]) + } + const isState = (node) => + isThis(node) || + (stateBinding !== null && context.isReference(node, stateBinding)) + const isField = (node, field, object) => + node?.type === 'MemberExpression' && + !node.computed && + propertyName(node.property) === field && + object(node.object) + + // The fetch: a local of the run entry initialized by reading the word field at an index. + const selectors = new Set() + for (const { node, path } of context.nodes(entry)) { + const read = + node.type === 'VariableDeclarator' + ? { id: node.id, value: node.init } + : node.type === 'AssignmentExpression' && + node.operator === '=' && + node.left.type === 'Identifier' + ? { id: node.left, value: node.right } + : null + if ( + read?.value?.type === 'MemberExpression' && + read.value.computed && + isField(read.value.object, wordsField, isState) && + path.getFunctionParent()?.node === entry + ) { + const binding = context.bindingFor(read.id) + if (binding) selectors.add(binding) + } + } + const switches = context + .nodes(entry) + .filter( + ({ node }) => + node.type === 'SwitchStatement' && + [...selectors].some((binding) => + context.isReference(node.discriminant, binding), + ), + ) + // A handler table dispatches by calling the state's member named by the fetched word. + const tableCalls = context + .nodes(entry) + .filter( + ({ node }) => + node.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + node.callee.computed && + isState(node.callee.object) && + [...selectors].some((binding) => + context.isReference(node.callee.property, binding), + ), + ) + if (switches.length + tableCalls.length !== 1) + decline('handler-locator', 'Dispatch on the fetched word is not unique') + const dispatch = (switches[0] ?? tableCalls[0]).node + + // A reader's receiver and its statements other than decoy writes. + const selfOf = (callable) => + callable.kind === 'method' + ? isThis + : (node) => + callable.fn.params[0]?.type === 'Identifier' && + context.isReference(node, callable.bindings[0]) + const effectiveBody = (callable) => + callable.fn.body.body.filter( + (statement) => + !( + statement.type === 'ExpressionStatement' && + callable.decoyWrites.includes(statement.expression) + ), + ) + // Operand reader: returns the word at a post-incremented index. + const operands = callables.filter((callable) => { + const body = effectiveBody(callable) + const value = body.length === 1 ? body[0].argument : null + return ( + body[0]?.type === 'ReturnStatement' && + value?.type === 'MemberExpression' && + value.computed && + value.property.type === 'UpdateExpression' && + value.property.operator === '++' && + !value.property.prefix && + isField(value.object, wordsField, selfOf(callable)) + ) + }) + if (operands.length !== 1) + decline('handler-locator', 'Operand reader is not unique') + const operandReader = operands[0] + const firstReal = (callable) => (callable.kind === 'function' ? 1 : 0) + const readerArguments = (node, reader) => { + if (calleeOf(node) !== reader) return null + const args = realArguments(node, reader) + if (reader.kind === 'function' && !isState(args[0])) return null + return args.slice(firstReal(reader)) + } + const operand = (node) => readerArguments(node, operandReader)?.length === 0 + + // Constant reader: reads the pool field; its two operand parameters default to operand reads on + // its own receiver (`C(a)`, `a.z(7)` in a devirtualized function, `this.Q()` in a method). + const constants = callables.filter( + (callable) => + callable !== operandReader && + context + .nodes(callable.fn) + .some(({ node }) => isField(node, poolField, selfOf(callable))), + ) + if (constants.length !== 1) + decline('handler-locator', 'Constant reader is not unique') + const constantReader = constants[0] + const defaulted = context.nodes(constantReader.fn).flatMap(({ node }) => { + const pair = + node.type === 'VariableDeclarator' + ? [node.id, node.init] + : node.type === 'AssignmentExpression' && node.operator === '=' + ? [node.left, node.right] + : null + const value = pair?.[1] + const readsOperand = (node) => + calleeOf(node) === operandReader || + (methodOf(node) === operandReader && + selfOf(constantReader)(node.callee.object)) + if ( + value?.type !== 'LogicalExpression' || + value.operator !== '??' || + !readsOperand(value.right) + ) + return [] + const index = constantReader.bindings.indexOf( + context.bindingFor(value.left), + ) + return index >= 0 ? [index] : [] + }) + const real = constantReader.bindings + .map((_, index) => index) + .filter( + (index) => + !constantReader.decoys.has(index) && index >= firstReal(constantReader), + ) + // Minify drops the pair when no handler passes it; the reader then reads both operands itself. + if ( + ![0, 2].includes(defaulted.length) || + new Set(defaulted).size !== defaulted.length || + defaulted.some((index) => !real.includes(index)) + ) + decline('handler-locator', 'Constant reader parameters are ambiguous') + // Explicit constant arguments in the reader's (index, key) order; a call passing none gives []. + const constant = (node) => { + if (calleeOf(node) !== constantReader) return null + if (node.arguments.length === firstReal(constantReader)) { + if (constantReader.kind === 'function' && !isState(node.arguments[0])) + return null + return [] + } + const args = realArguments(node, constantReader) + if (constantReader.kind === 'function' && !isState(node.arguments[0])) + return null + const kept = constantReader.bindings + .map((_, index) => index) + .filter((index) => !constantReader.decoys.has(index)) + if (args.length > kept.length) return null + return defaulted.map((index) => args[kept.indexOf(index)]) + } + + const handlers = ( + dispatch.type === 'SwitchStatement' + ? dispatch.cases + : tableHandlers(context, declarations, vm) + ).flatMap((item) => { + if (item.type === 'TableHandler') return [item] + if (!item.test) return [] + const label = labelValue(context, declarations, item.test) + if (label === null) + decline('handler-locator', 'Dispatch label has no value') + const statements = caseStatements(item) + // A case leaves the dispatch by `break`, or by ending the switch as its last case. + const ends = + statements.at(-1)?.type === 'BreakStatement' || + (item === dispatch.cases.at(-1) && !statements.some(t.isBreakStatement)) + return [{ label, entry: item, statements, ends, table: false }] + }) + if (new Set(handlers.map(({ label }) => label)).size !== handlers.length) + decline('handler-locator', 'Dispatch labels are not distinct') + + // Lower cloned handler statements for comparison: reader calls become `$operand()` and + // `$constant(...)` with their explicit arguments, other calls lose decoy arguments, and each + // statement-level write stands alone (`var x = v, y = w` and `x = v, y = w` read as `x = v; + // y = w`). `origins` maps clones to the runtime's own nodes. `nodes` clone some of a handler's + // `body`; for a table handler its prologue is cloned in front, and propagation consumes it. + const lower = (nodes, origins, handler = null) => { + if (handler?.table && handler.prologue.length > 0) + nodes.unshift(...cloneRuntimeFragment(handler.prologue, origins).nodes) + lowerShape(nodes, origins) + if (handler?.table) ssaPropagate(nodes, origins, context, handler.entry) + return nodes + } + const lowerShape = (nodes, origins) => { + const original = (node) => origins?.get(node) ?? node + // A File root gives every statement a parent path; the block keeps `nodes` as its body. + traverse(t.file(t.program([t.blockStatement(nodes)])), { + noScope: true, + CallExpression(path) { + const node = original(path.node) + const callable = calleeOf(node) ?? methodOf(node) + if (!callable) return + const position = (argument) => node.arguments.indexOf(argument) + const cloned = (argument) => path.node.arguments[position(argument)] + if (callable === operandReader && operand(node)) { + path.replaceWith(t.callExpression(t.identifier('$operand'), [])) + path.skip() + return + } + const args = callable === constantReader ? constant(node) : null + if (args) { + path.node.callee = t.identifier('$constant') + path.node.arguments = args.map(cloned) + return + } + path.node.arguments = realArguments(node, callable).map(cloned) + }, + NewExpression(path) { + const node = original(path.node) + const binding = + node.callee.type === 'Identifier' + ? context.bindingFor(node.callee) + : null + const callable = callables.find( + (item) => + item.kind === 'function' && binding && item.binding === binding, + ) + if (!callable) return + const position = (argument) => node.arguments.indexOf(argument) + path.node.arguments = realArguments(node, callable).map( + (argument) => path.node.arguments[position(argument)], + ) + }, + MemberExpression(path) { + // A read of the program's stable numeric map (`SLOTS.THIS`) is its value; class + // obfuscation and minify inline the same value. + const node = original(path.node) + if ( + node.computed || + t.isAssignmentExpression(path.parent, { left: path.node }) + ) + return + const value = labelValue(context, declarations, node) + if (value !== null && node.object.type === 'Identifier') + path.replaceWith(t.numericLiteral(value)) + }, + Identifier(path) { + // So is a read of a binding that stably holds a number (object extraction's spelling). + if ( + !path.isReferencedIdentifier() || + t.isAssignmentExpression(path.parent, { left: path.node }) || + t.isUpdateExpression(path.parent) + ) + return + const value = labelValue(context, declarations, original(path.node)) + if (value !== null) path.replaceWith(t.numericLiteral(value)) + }, + }) + normalizeStatements(nodes) + // An inlined decoy write stores only dummy literals into a named field; no handler of the + // runtime does that, so it is dropped. + const dummyOnly = (node) => + isDummyLiteral(node) || + (node?.type === 'ArrayExpression' && node.elements.every(dummyOnly)) + nodes.splice( + 0, + nodes.length, + ...nodes.filter( + (statement) => + !( + statement.type === 'ExpressionStatement' && + statement.expression.type === 'AssignmentExpression' && + statement.expression.operator === '=' && + statement.expression.left.type === 'MemberExpression' && + !statement.expression.left.computed && + dummyOnly(statement.expression.right) + ), + ), + ) + } + + // A table handler is its own function, so it opens by reading the frame state a switch case + // takes from the run function (`var regs = this._regs; var base = regs[this._f + 7]`): its + // prologue, the leading statements whose writes propagation consumes. `body` is the rest, the + // statements readers read; a case's body is all of its statements. + for (const handler of handlers) { + handler.prologue = handler.table + ? tablePrologue(handler, lowerShape, context) + : [] + handler.body = handler.statements.slice(handler.prologue.length) + } + + // This decode's opcode values; canonical until behaviour matching overwrites them. + const opcodes = createOpcodeTable() + return { + context, + dispatch, + handlers, + opcodes, + fields: { words: wordsField, pool: poolField }, + operand, + constant, + lower, + } +} + +// `T[label] = function () {...}` where T is the VM prototype or a stable alias of it. +function tableHandlers(context, declarations, vm) { + const isPrototype = (node) => + node?.type === 'MemberExpression' && + !node.computed && + propertyName(node.property) === 'prototype' && + context.isReference(node.object, vm.binding) + const aliases = new Set( + declarations + .filter( + (item) => + item.kind === 'variable' && + isStableDeclaration(item) && + isPrototype(item.init), + ) + .map((item) => item.binding), + ) + const handlers = [] + for (const statement of context.program.body) { + const node = statement.expression + if ( + statement.type !== 'ExpressionStatement' || + node?.type !== 'AssignmentExpression' || + node.operator !== '=' || + node.left.type !== 'MemberExpression' || + !node.left.computed || + node.right.type !== 'FunctionExpression' || + !( + isPrototype(node.left.object) || + aliases.has(context.bindingFor(node.left.object)) + ) + ) + continue + const label = labelValue(context, declarations, node.left.property) + if (label === null) + decline('handler-locator', 'Dispatch label has no value') + handlers.push({ + type: 'TableHandler', + label, + entry: node.right, + statements: node.right.body.body, + ends: true, + table: true, + }) + } + if (handlers.length === 0) + decline('handler-locator', 'Handler table has no handlers') + return handlers +} + +// Minify coalesces a table handler's locals: one variable holds the frame pointer and later the +// upvalue cell. On the handler's copy, each top-level write of a local starts a new version (SSA +// renaming), and a leading version holding a frame-state read is replaced by that read (copy +// propagation), so every handler spells its frame state the same way. A write nested in a loop or +// conditional starts no version: a loop-carried variable keeps one identity, and a version it can +// change before the local's next top-level write is not propagated. `origins` maps a kept +// identifier to its version, which matching binds like a binding. +function ssaPropagate(nodes, origins, context, fn) { + const bindingOf = (node) => context.bindingFor(origins.get(node) ?? node) + const local = (node) => { + const binding = node?.type === 'Identifier' ? bindingOf(node) : null + return binding && binding.scope.block === fn ? binding : null + } + // Frame state is read from the VM's own fields: a field (`this._regs`, `this._f`), or a field + // indexed by fields and literals (the register base `this._regs[this._f + 7]`). A program + // register is indexed from the register base, itself a read (`this._regs[base + 15]`). + const fieldIndex = (node) => + node?.type === 'NumericLiteral' || + (node?.type === 'MemberExpression' && + !node.computed && + node.object.type === 'ThisExpression') || + (node?.type === 'BinaryExpression' && + node.operator === '+' && + fieldIndex(node.left) && + fieldIndex(node.right)) + const frameState = (node) => + node?.type === 'MemberExpression' && + (node.computed + ? frameState(node.object) && fieldIndex(node.property) + : node.object.type === 'ThisExpression') + const copy = (node) => { + const { + nodes: [cloned], + origins: map, + } = cloneRuntimeFragment([node]) + const relink = (value, from) => { + if (Array.isArray(value)) + return value.forEach((item, k) => relink(item, from[k])) + if (!value || typeof value !== 'object' || !value.type) return + const source = origins.get(from) ?? map.get(value) + if (source) origins.set(value, source) + for (const key of t.VISITOR_KEYS[value.type] ?? []) + relink(value[key], from[key]) + } + relink(cloned, node) + return cloned + } + const versions = new Map() + const values = new Map() + const versionOf = (binding) => { + if (!versions.has(binding)) + versions.set(binding, { ssa: binding, index: 0 }) + return versions.get(binding) + } + // Read occurrences in a statement take the current version, or its pure value. + const rename = (root) => + traverse(t.file(t.program([root])), { + noScope: true, + Identifier(path) { + if ( + path.parentPath.isMemberExpression({ property: path.node }) && + !path.parent.computed + ) + return + if ( + path.parentPath.isObjectProperty({ key: path.node }) && + !path.parent.computed + ) + return + const binding = local(path.node) + if (!binding) return + const version = versionOf(binding) + if (values.has(version)) { + path.replaceWith(copy(values.get(version))) + path.skip() + } else origins.set(path.node, version) + }, + }) + // Per statement: the local its top-level write targets, and the locals it writes inside a nested + // statement, where their value can change. + const topTarget = (statement) => + statement.type === 'ExpressionStatement' && + statement.expression?.type === 'AssignmentExpression' + ? statement.expression.left + : null + const nestedAt = nodes.map((statement) => { + const top = topTarget(statement) + const written = new Set() + traverse(t.file(t.program([statement])), { + noScope: true, + 'AssignmentExpression|UpdateExpression'(path) { + const target = path.node.left ?? path.node.argument + const binding = target !== top ? local(target) : null + if (binding) written.add(binding) + }, + }) + return written + }) + // A version written at statement `index` keeps its value until the next top-level write of its + // local, unless a nested write changes it first. + const nestedWhileLive = (binding, index) => { + for (let next = index + 1; next < nodes.length; next++) { + if (nestedAt[next].has(binding)) return true + if (local(topTarget(nodes[next])) === binding) return false + } + return false + } + const kept = [] + let prologue = true + for (const [index, statement] of nodes.entries()) { + const write = statement.expression + const target = + statement.type === 'ExpressionStatement' && + write?.type === 'AssignmentExpression' && + write.operator === '=' + ? local(write.left) + : null + if (!target) { + prologue = false + rename(statement) + kept.push(statement) + continue + } + const holder = t.expressionStatement(write.right) + rename(holder) + write.right = holder.expression + const version = { ssa: target, index: versionOf(target).index + 1 } + versions.set(target, version) + // Only the leading frame-state reads propagate; a later one, a literal (a specialized + // handler's baked operand) or a register read is the handler's own. + if ( + prologue && + frameState(write.right) && + !nestedWhileLive(target, index) + ) { + values.set(version, write.right) + continue + } + prologue = false + origins.set(write.left, version) + kept.push(statement) + } + nodes.splice(0, nodes.length, ...kept) +} + +// A table handler's prologue: the statements before the one holding the first statement that +// propagation keeps. Each lowered statement is traced to the handler statement its nodes came from +// before propagation rewrites it. +function tablePrologue(handler, lowerShape, context) { + const owner = new Map() + const walk = (value, visit) => { + if (Array.isArray(value)) return value.forEach((item) => walk(item, visit)) + if (!value || typeof value !== 'object' || !value.type) return + if (visit(value)) return + for (const key of t.VISITOR_KEYS[value.type] ?? []) walk(value[key], visit) + } + handler.statements.forEach((statement, index) => + walk(statement, (node) => void owner.set(node, index)), + ) + const { nodes, origins } = cloneRuntimeFragment(handler.statements) + lowerShape(nodes, origins) + const from = new Map( + nodes.map((statement) => { + let index = null + walk(statement, (node) => { + if (index !== null) return true + index = owner.get(origins.get(node)) ?? null + return index !== null + }) + return [statement, index] + }), + ) + ssaPropagate(nodes, origins, context, handler.entry) + const first = + nodes.length > 0 ? from.get(nodes[0]) : handler.statements.length + return handler.statements.slice(0, first ?? 0) +} + +// Each statement-level write stands alone: `var x = v, y = w` (not a loop head) and `x = v, y = w` +// read as `x = v; y = w;`. In place on `nodes`; also used for matcher templates. +export function normalizeStatements(nodes) { + traverse(t.file(t.program([t.blockStatement(nodes)])), { + noScope: true, + // Brace spelling: a control body holding one simple statement is that statement + // (normalize-source.js applies the same view). + exit(path) { + for (const key of ['consequent', 'alternate', 'body']) { + const body = path.node[key] + if ( + body && + !Array.isArray(body) && + isControlBodyKey(path.node.type, key) && + simpleControlBody(body) !== body + ) + path.node[key] = simpleControlBody(body) + } + }, + VariableDeclaration(path) { + if (path.node.kind !== 'var' || t.isFor(path.parent)) return + path.replaceWithMultiple( + path.node.declarations + .filter((item) => item.init) + .map((item) => + t.expressionStatement( + t.assignmentExpression('=', item.id, item.init), + ), + ), + ) + }, + // A loop head's leading initializers run once before the loop: `for (var a = x, i = 0; ...)` + // reads as `a = x; for (var i = 0; ...)`, and likewise for a sequence. + ForStatement(path) { + const init = path.node.init + const leading = + init?.type === 'VariableDeclaration' && init.kind === 'var' + ? init.declarations + .slice(0, -1) + .filter((item) => item.init) + .map((item) => t.assignmentExpression('=', item.id, item.init)) + : init?.type === 'SequenceExpression' + ? init.expressions.slice(0, -1) + : [] + if (init?.type === 'VariableDeclaration' && leading.length > 0) + init.declarations = init.declarations.slice(-1) + else if (init?.type === 'SequenceExpression') + path.node.init = init.expressions.at(-1) + // `for (var x = v; ...)` reads as `for (x = v; ...)`: the var belongs to the function. + const rest = path.node.init + if ( + rest?.type === 'VariableDeclaration' && + rest.kind === 'var' && + rest.declarations.length === 1 && + rest.declarations[0].init + ) { + const [only] = rest.declarations + path.node.init = t.assignmentExpression('=', only.id, only.init) + } + if (leading.length > 0) + path.insertBefore(leading.map((item) => t.expressionStatement(item))) + }, + ExpressionStatement(path) { + if (path.node.expression.type !== 'SequenceExpression') return + path.replaceWithMultiple( + path.node.expression.expressions.map((item) => + t.expressionStatement(item), + ), + ) + }, + }) + return nodes +} + +// Canonical handlers carry a value of this decode's opcode table; the rest are added opcodes. +export function partitionHandlers(view) { + const nameByOpcode = new Map( + Object.entries(view.opcodes).map(([name, value]) => [value, name]), + ) + const canonical = [] + const extra = [] + for (const handler of view.handlers) { + const name = nameByOpcode.get(handler.label) + if (name) canonical.push({ name, opcode: handler.label, handler }) + else extra.push(handler) + } + extra.sort((left, right) => left.label - right.label) + return { canonical, extra, firstAdded: Math.max(...nameByOpcode.keys()) + 1 } +} + +// The view of the interpreter's handlers; a runtime the locator does not reach is not admitted. +export function requireHandlers(ast, code) { + try { + return locateHandlers(ast) + } catch (error) { + if (error instanceof ContainerDecline) + decline(code, 'Numeric container is not admitted') + throw error + } +} + +export function locateHandlers(ast) { + return locate(ast) +} + +export function diagnoseHandlers(ast) { + try { + const view = locate(ast) + return deepFreeze({ + ok: true, + result: { labels: view.handlers.map(({ label }) => label) }, + diagnostic: null, + }) + } catch (error) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: error.code ?? 'handler-locator-error', + message: error.message, + }, + }) + } +} diff --git a/src/vm/jsconfuser-vm/materialize-encoded-bytecode.js b/src/vm/jsconfuser-vm/materialize-encoded-bytecode.js new file mode 100644 index 00000000..6763770b --- /dev/null +++ b/src/vm/jsconfuser-vm/materialize-encoded-bytecode.js @@ -0,0 +1,24 @@ +import { decline } from './container-role-helpers.js' + +// The encoded transport's payload: canonical base64 of the words as u32 little-endian. +export function decodeCanonicalBytecodeWords(value) { + if ( + typeof value !== 'string' || + !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test( + value, + ) + ) { + decline('encoded-bytecode', 'Payload is not canonical base64') + } + const bytes = Buffer.from(value, 'base64') + if ( + bytes.length === 0 || + bytes.length % 4 !== 0 || + bytes.toString('base64') !== value + ) { + decline('encoded-bytecode', 'Payload is not a u32 LE byte stream') + } + return Array.from({ length: bytes.length / 4 }, (_, index) => + bytes.readUInt32LE(index * 4), + ) +} diff --git a/src/vm/jsconfuser-vm/minified-runtime-container.js b/src/vm/jsconfuser-vm/minified-runtime-container.js new file mode 100644 index 00000000..fb4018e6 --- /dev/null +++ b/src/vm/jsconfuser-vm/minified-runtime-container.js @@ -0,0 +1,51 @@ +import { deepFreeze } from './container-role-helpers.js' + +const scalar = (value) => ({ name: null, value }) + +export function minifiedRuntimeContainer(observation) { + return deepFreeze({ + schemaVersion: 'jsconfuser-vm-container.v1', + encoding: 'numeric-u32', + roles: { + pool: { name: null, values: [...observation.pool] }, + words: { name: null, values: [...observation.words] }, + op: { name: null, values: { ...observation.opcodes } }, + sentinels: { name: null, values: { ...observation.sentinels } }, + slots: { name: null, values: { ...observation.slots } }, + scalars: { + MAIN_START_PC: scalar(observation.root.startPc), + MAIN_REG_COUNT: scalar(observation.root.regCount), + ENCODE_BYTECODE: scalar(false), + TIMING_CHECKS: scalar(false), + HEADER_SIZE: scalar(observation.frame.headerSize), + FRAME_START: scalar(observation.frame.frameStart), + }, + constructors: { + Upvalue: { name: null }, + Closure: { name: null }, + VM: { name: null }, + }, + methods: { + upvalue: Object.fromEntries( + ['read', 'write', 'close'].map((role) => [role, { name: null }]), + ), + vm: Object.fromEntries( + [ + 'operand', + 'pushFrame', + 'captureUpvalue', + 'constant', + 'closeUpvalues', + 'run', + ].map((role) => [role, { name: null }]), + ), + }, + roots: { + globals: { name: null }, + vm: { name: null }, + decoder: { name: null }, + bootMethod: { name: null }, + }, + }, + }) +} diff --git a/src/vm/jsconfuser-vm/runtime-fragment-pattern.js b/src/vm/jsconfuser-vm/runtime-fragment-pattern.js new file mode 100644 index 00000000..ab754658 --- /dev/null +++ b/src/vm/jsconfuser-vm/runtime-fragment-pattern.js @@ -0,0 +1,22 @@ +import * as t from '@babel/types' + +// Deep clones of `nodes`, each mapped to its original in `origins` (a new map unless one is given). +export function cloneRuntimeFragment(nodes, origins = new WeakMap()) { + const associate = (original, cloned) => { + if (!original || !cloned) return + origins.set(cloned, original) + for (const key of t.VISITOR_KEYS[original.type] ?? []) { + if (Array.isArray(original[key])) { + original[key].forEach((child, index) => + associate(child, cloned[key][index]), + ) + } else associate(original[key], cloned[key]) + } + } + const cloned = nodes.map((node) => { + const result = t.cloneNode(node, true) + associate(node, result) + return result + }) + return { nodes: cloned, origins } +} diff --git a/src/vm/jsconfuser-vm/runtime-source-signature.js b/src/vm/jsconfuser-vm/runtime-source-signature.js new file mode 100644 index 00000000..50979971 --- /dev/null +++ b/src/vm/jsconfuser-vm/runtime-source-signature.js @@ -0,0 +1,45 @@ +import crypto from 'node:crypto' + +export const BASE64_SHA256 = + '2cbdb5905ce7889c55b6672a05631b9f7f0470ff72577a07ec4b44b65bd61e07' + +const OMIT = new Set([ + 'start', + 'end', + 'loc', + 'extra', + 'leadingComments', + 'trailingComments', + 'innerComments', +]) + +function canonical(value, context, bindings) { + if (Array.isArray(value)) + return value.map((child) => canonical(child, context, bindings)) + if (value === null || typeof value !== 'object') return value + const result = Object.fromEntries( + Object.keys(value) + .sort() + .filter((key) => !OMIT.has(key)) + .map((key) => [key, canonical(value[key], context, bindings)]), + ) + const path = context.paths.get(value) + const binding = + value.type === 'Identifier' && + path && + (path.isBindingIdentifier() || path.isReferencedIdentifier()) + ? context.bindingFor(value) + : null + if (binding) { + if (!bindings.has(binding)) bindings.set(binding, bindings.size) + result.name = `binding:${bindings.get(binding)}` + } + return result +} + +export function runtimeSignature(node, context) { + return crypto + .createHash('sha256') + .update(JSON.stringify(canonical(node, context, new Map()))) + .digest('hex') +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/encoded.js new file mode 100644 index 00000000..afa88c54 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"_value", /* 1 */2, /* 2 */"value", /* 3 */"push", /* 4 */"rhs", /* 5 */3, /* 6 */"window", /* 7 */"TEST_OUTPUT", /* 8 */"events", /* 9 */undefined, /* 10 */"key", /* 11 */"get", /* 12 */"set"]; +var BYTECODE = [4, 1, 47, 5, 153, 0, 6, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 0, 5, 0, 0, 0, 6, 1, 0, 49, 7, 1, 5, 6, 0, 5, 2, 0, 47, 6, 188, 0, 6, 1, 0, 1, 2, 50, 7, 5, 6, 0, 5, 2, 0, 47, 6, 227, 1, 7, 1, 0, 1, 2, 51, 7, 5, 6, 5, 4, 7, 42, 5, 3, 0, 0, 6, 2, 0, 8, 7, 4, 6, 0, 6, 3, 0, 8, 8, 2, 6, 0, 6, 4, 0, 43, 9, 2, 8, 1, 6, 0, 6, 5, 0, 11, 8, 7, 6, 9, 4, 5, 8, 2, 5, 6, 0, 0, 6, 7, 0, 0, 7, 2, 0, 0, 8, 2, 0, 8, 9, 4, 8, 0, 8, 8, 0, 49, 10, 2, 7, 9, 8, 2, 9, 5, 6, 10, 0, 5, 9, 0, 45, 5, 4, 1, 3, 2, 0, 0, 3, 3, 0, 8, 4, 2, 3, 0, 3, 10, 0, 43, 5, 2, 4, 1, 3, 0, 2, 2, 0, 45, 2, 0, 2, 9, 0, 45, 2, 4, 1, 3, 2, 0, 0, 3, 3, 0, 8, 4, 2, 3, 0, 3, 11, 0, 43, 5, 2, 4, 1, 3, 0, 2, 0, 0, 8, 3, 1, 2, 45, 3, 0, 2, 9, 0, 45, 2, 4, 2, 3, 3, 0, 0, 4, 3, 0, 8, 5, 3, 4, 0, 4, 12, 0, 43, 6, 3, 5, 1, 4, 0, 3, 0, 0, 9, 2, 3, 0, 0, 3, 9, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/oracle.json new file mode 100644 index 00000000..ee65bc3c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/oracle.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "composition-accessor-evaluation-order", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": { + "value": 5, + "events": [ + "key", + "get", + "rhs", + "set", + "get" + ] + }, + "expectedThrow": null, + "semanticTags": [ + "computed-property", + "getter", + "setter", + "receiver-preservation", + "assignment-evaluation-order", + "observable-side-effect-order" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged JSON-safe projection excludes identity-only assertions and host-dependent stack data.", + "The accessor event list is the observable that distinguishes computed-key/getter/RHS/setter evaluation order." + ], + "exclusions": [ + "Encoder internals, debug comments, and decoder behavior are not used to define the expected observable.", + "The try/finally cell remains a source-backed unsupported boundary if the registered decoder declines it." + ], + "comparison": { + "sourceExpected": "exact-normalized-observable", + "recoveredExpected": "exact-normalized-observable on success only" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/source.js new file mode 100644 index 00000000..cbb2d154 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-accessor-evaluation-order/source.js @@ -0,0 +1,9 @@ +var events = []; +function key() { events.push("key"); return "value"; } +var object = { + _value: 2, + get value() { events.push("get"); return this._value; }, + set value(next) { events.push("set"); this._value = next; } +}; +object[key()] = object.value + (events.push("rhs"), 3); +window.TEST_OUTPUT = { value: object.value, events: events }; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/encoded.js new file mode 100644 index 00000000..aaabe582 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */4, /* 1 */2, /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */1, /* 5 */undefined]; +var BYTECODE = [4, 1, 47, 6, 77, 1, 5, 0, 0, 5, 2, 6, 0, 6, 0, 0, 42, 7, 2, 1, 6, 5, 3, 7, 0, 6, 1, 0, 42, 7, 3, 1, 6, 5, 4, 7, 42, 6, 3, 1, 4, 5, 5, 6, 2, 6, 2, 0, 0, 7, 3, 0, 0, 8, 4, 0, 42, 9, 3, 1, 8, 48, 8, 3, 4, 5, 9, 9, 6, 7, 8, 0, 6, 5, 0, 45, 6, 4, 2, 5, 3, 0, 47, 4, 99, 1, 5, 1, 0, 1, 3, 45, 4, 0, 4, 5, 0, 45, 4, 4, 2, 3, 3, 0, 11, 4, 3, 0, 7, 0, 4, 3, 3, 0, 45, 3, 0, 3, 5, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/oracle.json new file mode 100644 index 00000000..1ab7fa23 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/oracle.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "composition-closure-upvalue", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 6, + 12, + 13 + ], + "expectedThrow": null, + "semanticTags": [ + "returned-closure", + "upvalue", + "mutable-captured-state", + "repeated-call", + "call-result-feeds-next-call" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged JSON-safe projection excludes identity-only assertions and host-dependent stack data.", + "The accessor event list is the observable that distinguishes computed-key/getter/RHS/setter evaluation order." + ], + "exclusions": [ + "Encoder internals, debug comments, and decoder behavior are not used to define the expected observable.", + "The try/finally cell remains a source-backed unsupported boundary if the registered decoder declines it." + ], + "comparison": { + "sourceExpected": "exact-normalized-observable", + "recoveredExpected": "exact-normalized-observable on success only" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/source.js new file mode 100644 index 00000000..d9896278 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-closure-upvalue/source.js @@ -0,0 +1,8 @@ +function makeAccumulator(seed) { + var total = seed; + return function (step) { total += step; return total; }; +} +var advance = makeAccumulator(4); +var first = advance(2); +var second = advance(first); +window.TEST_OUTPUT = [first, second, advance(1)]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/encoded.js new file mode 100644 index 00000000..c0212ba7 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */true, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */undefined, /* 4 */"push", /* 5 */"try-", /* 6 */"returned", /* 7 */"normal", /* 8 */"finally-"]; +var BYTECODE = [4, 1, 47, 5, 55, 1, 9, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 0, 5, 0, 0, 42, 6, 3, 1, 5, 5, 4, 6, 2, 5, 1, 0, 0, 6, 2, 0, 48, 7, 2, 4, 2, 9, 5, 6, 7, 0, 5, 3, 0, 45, 5, 4, 2, 59, 122, 3, 4, 149, 3, 5, 0, 0, 6, 4, 0, 8, 7, 5, 6, 0, 6, 5, 0, 11, 8, 6, 0, 43, 6, 5, 7, 1, 8, 40, 0, 103, 0, 5, 6, 0, 5, 4, 5, 1, 3, 151, 55, 39, 122, 0, 5, 7, 0, 5, 4, 5, 1, 3, 153, 55, 39, 122, 55, 1, 3, 155, 39, 122, 3, 5, 0, 0, 6, 4, 0, 8, 7, 5, 6, 0, 6, 8, 0, 11, 8, 6, 0, 43, 6, 5, 7, 1, 8, 58, 3, 46, 4, 45, 4, 45, 4, 0, 5, 3, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/oracle.json new file mode 100644 index 00000000..036ce4c3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/oracle.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "composition-finally-abrupt-return", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + "returned", + [ + "try-true", + "finally-true" + ] + ], + "expectedThrow": null, + "semanticTags": [ + "try-finally", + "abrupt-return", + "completion-order", + "finally-side-effect" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged JSON-safe projection excludes identity-only assertions and host-dependent stack data.", + "The accessor event list is the observable that distinguishes computed-key/getter/RHS/setter evaluation order." + ], + "exclusions": [ + "Encoder internals, debug comments, and decoder behavior are not used to define the expected observable.", + "The try/finally cell remains a source-backed unsupported boundary if the registered decoder declines it." + ], + "comparison": { + "sourceExpected": "exact-normalized-observable", + "recoveredExpected": "exact-normalized-observable on success only" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/source.js new file mode 100644 index 00000000..fe089f70 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-finally-abrupt-return/source.js @@ -0,0 +1,12 @@ +var log = []; +function run(flag) { + try { + log.push("try-" + flag); + if (flag) return "returned"; + return "normal"; + } finally { + log.push("finally-" + flag); + } +} +var result = run(true); +window.TEST_OUTPUT = [result, log]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/encoded.js new file mode 100644 index 00000000..0b46f9e5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */1, /* 2 */4, /* 3 */"push", /* 4 */3, /* 5 */"window", /* 6 */"TEST_OUTPUT", /* 7 */"trace", /* 8 */"total", /* 9 */undefined, /* 10 */2]; +var BYTECODE = [4, 1, 47, 7, 154, 1, 7, 0, 0, 5, 2, 7, 48, 7, 0, 5, 3, 7, 0, 7, 0, 0, 5, 4, 7, 0, 7, 1, 0, 5, 5, 7, 0, 7, 2, 0, 24, 8, 5, 7, 40, 8, 121, 42, 7, 2, 1, 5, 5, 6, 7, 0, 7, 3, 0, 8, 8, 3, 7, 43, 7, 3, 8, 1, 6, 0, 7, 4, 0, 23, 8, 6, 7, 40, 8, 98, 0, 7, 1, 0, 12, 8, 6, 7, 42, 7, 2, 1, 8, 11, 8, 4, 7, 5, 4, 8, 39, 105, 11, 7, 4, 6, 5, 4, 7, 5, 7, 5, 0, 7, 1, 0, 11, 8, 5, 7, 5, 5, 8, 39, 32, 2, 7, 5, 0, 0, 8, 6, 0, 0, 9, 7, 0, 0, 10, 8, 0, 49, 11, 2, 9, 3, 10, 4, 9, 7, 8, 11, 0, 7, 9, 0, 45, 7, 4, 2, 0, 3, 10, 0, 15, 4, 0, 3, 0, 3, 0, 0, 26, 5, 4, 3, 40, 5, 188, 0, 3, 10, 0, 14, 4, 0, 3, 5, 3, 4, 39, 207, 0, 4, 4, 0, 13, 5, 0, 4, 0, 4, 1, 0, 11, 6, 5, 4, 5, 3, 6, 45, 3, 0, 3, 9, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/oracle.json new file mode 100644 index 00000000..47bbc8be --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/oracle.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "composition-loop-dependent-calls", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": { + "trace": [ + 4, + 1, + 10, + 2 + ], + "total": 41 + }, + "expectedThrow": null, + "semanticTags": [ + "for-loop", + "call-in-loop", + "call-result-branch", + "dependent-second-call", + "method-mutation" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged JSON-safe projection excludes identity-only assertions and host-dependent stack data.", + "The accessor event list is the observable that distinguishes computed-key/getter/RHS/setter evaluation order." + ], + "exclusions": [ + "Encoder internals, debug comments, and decoder behavior are not used to define the expected observable.", + "The try/finally cell remains a source-backed unsupported boundary if the registered decoder declines it." + ], + "comparison": { + "sourceExpected": "exact-normalized-observable", + "recoveredExpected": "exact-normalized-observable on success only" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/source.js new file mode 100644 index 00000000..df18de24 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-loop-dependent-calls/source.js @@ -0,0 +1,10 @@ +function classify(value) { return value % 2 === 0 ? value / 2 : value * 3 + 1; } +var trace = []; +var total = 0; +for (var i = 1; i <= 4; i++) { + var transformed = classify(i); + trace.push(transformed); + if (transformed > 3) total += classify(transformed - 1); + else total += transformed; +} +window.TEST_OUTPUT = { trace: trace, total: total }; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/encoded.js new file mode 100644 index 00000000..40485ac2 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"prototype", /* 1 */"add", /* 2 */"read", /* 3 */2, /* 4 */5, /* 5 */3, /* 6 */"value", /* 7 */1, /* 8 */"window", /* 9 */"TEST_OUTPUT", /* 10 */undefined]; +var BYTECODE = [4, 1, 47, 7, 213, 1, 4, 0, 0, 5, 2, 7, 0, 7, 0, 0, 8, 8, 2, 7, 0, 7, 1, 0, 47, 9, 229, 1, 6, 0, 0, 9, 8, 7, 9, 0, 7, 0, 0, 8, 8, 2, 7, 0, 7, 2, 0, 47, 9, 255, 0, 4, 0, 0, 9, 8, 7, 9, 0, 7, 3, 0, 44, 8, 2, 1, 7, 5, 3, 8, 0, 7, 4, 0, 44, 8, 2, 1, 7, 5, 4, 8, 0, 7, 1, 0, 8, 8, 3, 7, 0, 7, 5, 0, 43, 9, 3, 8, 1, 7, 0, 7, 1, 0, 8, 8, 9, 7, 0, 7, 6, 0, 8, 10, 3, 7, 43, 7, 9, 8, 1, 10, 0, 8, 2, 0, 8, 9, 7, 8, 43, 8, 7, 9, 0, 5, 5, 8, 0, 7, 1, 0, 8, 8, 4, 7, 0, 7, 7, 0, 43, 9, 4, 8, 1, 7, 0, 7, 2, 0, 8, 8, 9, 7, 43, 7, 9, 8, 0, 5, 6, 7, 2, 7, 8, 0, 0, 8, 9, 0, 0, 9, 6, 0, 8, 10, 3, 9, 0, 9, 6, 0, 8, 11, 4, 9, 48, 9, 4, 5, 6, 10, 11, 9, 7, 8, 9, 0, 7, 10, 0, 45, 7, 4, 2, 0, 3, 6, 0, 9, 2, 3, 0, 0, 3, 10, 0, 45, 3, 4, 2, 0, 3, 6, 0, 8, 4, 2, 3, 11, 5, 4, 0, 9, 2, 3, 5, 45, 2, 0, 3, 10, 0, 45, 3, 4, 1, 0, 2, 6, 0, 8, 3, 1, 2, 45, 3, 0, 2, 10, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/oracle.json new file mode 100644 index 00000000..be6b2e62 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/oracle.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "composition-receiver-mutation", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 10, + 6, + 10, + 6 + ], + "expectedThrow": null, + "semanticTags": [ + "receiver-preservation", + "method-call", + "property-mutation", + "chained-call", + "final-observation-depends-on-receiver-and-mutation" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged JSON-safe projection excludes identity-only assertions and host-dependent stack data.", + "The accessor event list is the observable that distinguishes computed-key/getter/RHS/setter evaluation order." + ], + "exclusions": [ + "Encoder internals, debug comments, and decoder behavior are not used to define the expected observable.", + "The try/finally cell remains a source-backed unsupported boundary if the registered decoder declines it." + ], + "comparison": { + "sourceExpected": "exact-normalized-observable", + "recoveredExpected": "exact-normalized-observable on success only" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/source.js new file mode 100644 index 00000000..ab5a9ab2 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/composition/composition-receiver-mutation/source.js @@ -0,0 +1,8 @@ +function Box(seed) { this.value = seed; } +Box.prototype.add = function (delta) { this.value += delta; return this; }; +Box.prototype.read = function () { return this.value; }; +var left = new Box(2); +var right = new Box(5); +var leftResult = left.add(3).add(left.value).read(); +var rightResult = right.add(1).read(); +window.TEST_OUTPUT = [leftResult, rightResult, left.value, right.value]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/encoded.js new file mode 100644 index 00000000..c3eda5e0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */"finished", /* 5 */undefined, /* 6 */0, /* 7 */"Error", /* 8 */"negative", /* 9 */"message", /* 10 */true]; +var BYTECODE = [4, 1, 47, 3, 69, 1, 9, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 32, 6, 5, 42, 5, 2, 1, 6, 0, 6, 3, 0, 42, 7, 2, 1, 6, 2, 6, 0, 0, 0, 8, 4, 0, 8, 9, 6, 8, 48, 6, 3, 5, 7, 9, 9, 3, 4, 6, 0, 3, 5, 0, 45, 3, 4, 2, 59, 149, 4, 5, 167, 54, 126, 3, 0, 6, 6, 0, 22, 7, 0, 6, 40, 7, 105, 2, 6, 7, 0, 0, 7, 8, 0, 44, 8, 6, 1, 7, 46, 8, 0, 6, 2, 0, 11, 7, 0, 6, 55, 5, 5, 7, 1, 4, 169, 55, 39, 149, 55, 39, 143, 0, 6, 9, 0, 8, 7, 3, 6, 5, 5, 7, 1, 4, 171, 55, 39, 149, 55, 1, 4, 173, 39, 149, 2, 6, 0, 0, 0, 7, 4, 0, 0, 8, 10, 0, 9, 6, 7, 8, 58, 4, 46, 5, 45, 5, 45, 5, 0, 6, 5, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/oracle.json new file mode 100644 index 00000000..c74aa921 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/oracle.json @@ -0,0 +1,62 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "caught-return-through-finally", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + "negative", + 5, + true + ], + "expectedThrow": null, + "semanticTags": [ + "conditional", + "catch", + "finally", + "return" + ], + "provenance": { + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "seed": 1, + "rng": "mulberry32", + "options": { + "target": "browser", + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false + }, + "generatedAt": null, + "nodeVersion": null, + "toolSha256": null, + "executionProvenance": null + }, + "hazards": [ + "Finite synchronous VM execution with timeout; the fixture compares declared TEST_OUTPUT only." + ], + "exclusions": [ + "This fixture proves one conditional catch with two return continuations through a straight finalizer, not arbitrary nested handlers." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/caught-return-through-finally/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/encoded.js new file mode 100644 index 00000000..8b552265 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */1, /* 1 */2, /* 2 */3, /* 3 */4, /* 4 */5, /* 5 */"push", /* 6 */"concat", /* 7 */"window", /* 8 */"TEST_OUTPUT", /* 9 */undefined]; +var BYTECODE = [4, 1, 0, 4, 0, 0, 0, 5, 1, 0, 48, 6, 2, 4, 5, 5, 2, 6, 0, 4, 2, 0, 0, 5, 3, 0, 0, 6, 4, 0, 48, 7, 3, 4, 5, 6, 5, 3, 7, 0, 4, 5, 0, 8, 5, 2, 4, 48, 4, 0, 0, 6, 6, 0, 8, 7, 4, 6, 43, 6, 4, 7, 1, 3, 43, 4, 2, 5, 65535, 6, 2, 4, 7, 0, 0, 5, 8, 0, 9, 4, 5, 2, 0, 4, 9, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/oracle.json new file mode 100644 index 00000000..198557e1 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/oracle.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "focused-call-method-spread", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 1, + 2, + 3, + 4, + 5 + ], + "expectedThrow": null, + "semanticTags": [ + "spread-element", + "method-call", + "call-spread-sentinel", + "receiver-preservation" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged JSON-safe projection compares the declared TEST_OUTPUT value only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and decoder behavior are excluded from this focused fixture." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/source.js new file mode 100644 index 00000000..ebf72b2f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/source.js @@ -0,0 +1 @@ +var arr = [1, 2]; var more = [3, 4, 5]; arr.push(...more); window.TEST_OUTPUT = arr; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/encoded.js new file mode 100644 index 00000000..f15b0a2e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */10, /* 1 */20, /* 2 */"concat", /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */"x", /* 6 */"y", /* 7 */undefined]; +var BYTECODE = [4, 1, 47, 5, 92, 2, 5, 0, 0, 5, 2, 5, 0, 5, 0, 0, 0, 6, 1, 0, 48, 7, 2, 5, 6, 5, 3, 7, 48, 5, 0, 0, 6, 2, 0, 8, 7, 5, 6, 43, 6, 5, 7, 1, 3, 44, 5, 2, 65535, 6, 5, 4, 5, 2, 5, 3, 0, 0, 6, 4, 0, 0, 7, 5, 0, 8, 8, 4, 7, 0, 7, 6, 0, 8, 9, 4, 7, 48, 7, 2, 8, 9, 9, 5, 6, 7, 0, 5, 7, 0, 45, 5, 4, 3, 0, 4, 5, 0, 9, 3, 4, 0, 0, 4, 6, 0, 9, 3, 4, 1, 0, 4, 7, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/oracle.json new file mode 100644 index 00000000..0b1a02d3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/oracle.json @@ -0,0 +1,34 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "focused-new-spread", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 10, + 20 + ], + "expectedThrow": null, + "semanticTags": [ + "spread-element", + "constructor-call", + "new-spread-sentinel", + "constructed-fields" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged JSON-safe projection compares the declared TEST_OUTPUT value only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and decoder behavior are excluded from this focused fixture." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/source.js new file mode 100644 index 00000000..0824aa17 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/source.js @@ -0,0 +1 @@ +function Point(x, y) { this.x = x; this.y = y; } var coords = [10, 20]; var p = new Point(...coords); window.TEST_OUTPUT = [p.x, p.y]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/encoded.js new file mode 100644 index 00000000..b8d4f5d8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */17, /* 1 */"TEST_OUTPUT", /* 2 */"window", /* 3 */undefined]; +var BYTECODE = [4, 1, 0, 3, 0, 0, 5, 2, 3, 6, 1, 0, 2, 2, 3, 2, 0, 0, 4, 1, 0, 2, 5, 1, 0, 9, 3, 4, 5, 0, 3, 3, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 6; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/oracle.json new file mode 100644 index 00000000..078505f0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/oracle.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "focused-store-global-assignment", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": 17, + "expectedThrow": null, + "semanticTags": [ + "undeclared-global-assignment", + "store-global", + "global-readback" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged JSON-safe projection compares the declared TEST_OUTPUT value only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and decoder behavior are excluded from this focused fixture." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/source.js new file mode 100644 index 00000000..56cbed2c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/source.js @@ -0,0 +1 @@ +var value = 17; TEST_OUTPUT = value; window.TEST_OUTPUT = TEST_OUTPUT; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/encoded.js new file mode 100644 index 00000000..6ca54ac1 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */3, /* 3 */true, /* 4 */"4", /* 5 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 32, 5, 4, 33, 4, 5, 0, 5, 3, 0, 33, 6, 5, 0, 5, 4, 0, 33, 7, 5, 48, 5, 3, 4, 6, 7, 9, 2, 3, 5, 0, 2, 5, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/oracle.json new file mode 100644 index 00000000..218ba4a3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/oracle.json @@ -0,0 +1,34 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "focused-unary-positive-coercion", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + -3, + 1, + 4 + ], + "expectedThrow": null, + "semanticTags": [ + "unary-plus", + "numeric-coercion", + "operator-family" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged JSON-safe projection compares the declared TEST_OUTPUT value only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and decoder behavior are excluded from this focused fixture." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/source.js new file mode 100644 index 00000000..768f8965 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = [+(-3), +true, +"4"]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/encoded.js new file mode 100644 index 00000000..e9bc4eeb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */undefined, /* 5 */0, /* 6 */"Error", /* 7 */"negative", /* 8 */"message"]; +var BYTECODE = [4, 1, 47, 3, 56, 1, 7, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 32, 6, 5, 42, 5, 2, 1, 6, 0, 6, 3, 0, 42, 7, 2, 1, 6, 48, 6, 2, 5, 7, 9, 3, 4, 6, 0, 3, 4, 0, 45, 3, 4, 2, 54, 101, 3, 0, 4, 5, 0, 22, 5, 0, 4, 40, 5, 87, 2, 4, 6, 0, 0, 5, 7, 0, 44, 6, 4, 1, 5, 46, 6, 0, 4, 2, 0, 11, 5, 0, 4, 55, 45, 5, 55, 39, 111, 0, 4, 8, 0, 8, 5, 3, 4, 45, 5, 0, 4, 4, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/oracle.json new file mode 100644 index 00000000..8d483b6a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/oracle.json @@ -0,0 +1,61 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "protected-conditional-catch", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + "negative", + 5 + ], + "expectedThrow": null, + "semanticTags": [ + "conditional", + "catch", + "throw", + "normal-return" + ], + "provenance": { + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "seed": 1, + "rng": "mulberry32", + "options": { + "target": "browser", + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false + }, + "generatedAt": null, + "nodeVersion": null, + "toolSha256": null, + "executionProvenance": null + }, + "hazards": [ + "Finite synchronous VM execution with timeout; the fixture compares declared TEST_OUTPUT only." + ], + "exclusions": [ + "This fixture proves the conditional catch/normal return split for the pinned numeric baseline, not finalizer or hardening shapes." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/source.js new file mode 100644 index 00000000..6403185a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/protected-conditional-catch/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } } window.TEST_OUTPUT = [check(-1), check(4)]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/encoded.js new file mode 100644 index 00000000..e99a8ad9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */4, /* 4 */undefined, /* 5 */1]; +var BYTECODE = [4, 1, 47, 4, 53, 1, 8, 1, 0, 1, 2, 5, 3, 4, 0, 4, 0, 0, 5, 2, 4, 2, 4, 1, 0, 0, 5, 2, 0, 0, 6, 3, 0, 42, 7, 3, 1, 6, 48, 6, 2, 7, 2, 9, 4, 5, 6, 0, 4, 4, 0, 45, 4, 4, 2, 59, 83, 3, 4, 102, 0, 5, 5, 0, 11, 6, 0, 5, 5, 4, 6, 1, 3, 104, 55, 39, 83, 55, 1, 3, 106, 39, 83, 3, 5, 0, 5, 6, 5, 0, 6, 5, 0, 11, 7, 5, 6, 7, 0, 7, 58, 3, 46, 4, 45, 4, 0, 5, 4, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/oracle.json new file mode 100644 index 00000000..d42c2adf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/oracle.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "return-through-finally", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 5, + 1 + ], + "expectedThrow": null, + "semanticTags": [ + "finally", + "return", + "normal-return" + ], + "provenance": { + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "seed": 1, + "rng": "mulberry32", + "options": { + "target": "browser", + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false + }, + "generatedAt": null, + "nodeVersion": null, + "toolSha256": null, + "executionProvenance": null + }, + "hazards": [ + "Finite synchronous VM execution with timeout; the fixture compares declared TEST_OUTPUT only." + ], + "exclusions": [ + "This fixture proves one direct return through a straight finalizer, not multiple continuations or nested handlers." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/source.js new file mode 100644 index 00000000..747d80bf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/return-through-finally/source.js @@ -0,0 +1 @@ +let count = 0; function check(value) { try { return value + 1; } finally { count++; } } window.TEST_OUTPUT = [check(4), count]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/encoded.js new file mode 100644 index 00000000..d13e47a1 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */3, /* 4 */undefined, /* 5 */2, /* 6 */"one", /* 7 */"two", /* 8 */"other"]; +var BYTECODE = [4, 1, 47, 3, 53, 1, 5, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 42, 6, 2, 1, 5, 0, 5, 3, 0, 42, 7, 2, 1, 5, 48, 5, 2, 6, 7, 9, 3, 4, 5, 0, 3, 4, 0, 45, 3, 4, 2, 0, 3, 2, 0, 26, 4, 0, 3, 40, 4, 68, 39, 83, 0, 3, 5, 0, 26, 4, 0, 3, 40, 4, 81, 39, 89, 39, 95, 0, 3, 6, 0, 45, 3, 0, 3, 7, 0, 45, 3, 0, 3, 8, 0, 45, 3, 0, 3, 4, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/oracle.json new file mode 100644 index 00000000..4c11c910 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/oracle.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "terminal-switch-arms", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + "one", + "other" + ], + "expectedThrow": null, + "semanticTags": [ + "switch", + "disjoint-terminal-arms", + "structured-control" + ], + "provenance": { + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "seed": 1, + "rng": "mulberry32", + "options": { + "target": "browser", + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false + }, + "generatedAt": null, + "nodeVersion": null, + "toolSha256": null, + "executionProvenance": null + }, + "hazards": [ + "Finite synchronous VM execution with timeout; the fixture compares declared TEST_OUTPUT only." + ], + "exclusions": [ + "This fixture proves terminal switch-arm recovery for the pinned numeric baseline, not other switch shapes or option profiles." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/source.js new file mode 100644 index 00000000..98a67db9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/focused/terminal-switch-arms/source.js @@ -0,0 +1 @@ +function classify(value) { switch (value) { case 1: return "one"; case 2: return "two"; default: return "other"; } } window.TEST_OUTPUT = [classify(1), classify(3)]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/encoded.js new file mode 100644 index 00000000..90be5d6c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */2, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */1, /* 4 */undefined]; +var BYTECODE = [4,1,47,4,65,1,5,0,0,5,2,4,0,4,0,0,42,5,2,1,4,5,3,5,2,4,1,0,0,5,2,0,0,6,3,0,42,7,3,1,6,0,6,0,0,42,8,3,1,6,48,6,2,7,8,9,4,5,6,0,4,4,0,45,4,4,2,5,3,0,47,4,87,1,5,1,0,1,4294967295,45,4,0,4,4,0,45,4,4,2,3,3,0,11,4,3,0,7,0,4,3,3,0,45,3,0,3,4,0,45,3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/oracle.json new file mode 100644 index 00000000..0548e457 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "closure-invalid-capture", + "expectedStatus": "declined", + "expectedDiagnostic": "references-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "A captured-register descriptor points outside the captured function's register file.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/Closures.test.js:4-110" + ], + "semanticTags": [ + "closure-reference", + "capture-descriptor", + "out-of-range" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/source.js new file mode 100644 index 00000000..1a18c894 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/closure-invalid-capture/source.js @@ -0,0 +1,3 @@ +function make(seed) { var total = seed; return function (step) { total += step; return total; }; } +var next = make(2); +window.TEST_OUTPUT = [next(1), next(2)]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/encoded.js new file mode 100644 index 00000000..6c5798cb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */2, /* 1 */1, /* 2 */0, /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */undefined]; +var BYTECODE = [4,1,0,3,0,0,5,2,3,0,3,1,0,23,4,2,3,40,4,0,0,3,1,0,11,4,2,3,5,2,4,39,40,0,3,2,0,5,2,3,2,3,3,0,0,4,4,0,9,3,4,2,0,3,5,0,45,3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 5; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/oracle.json new file mode 100644 index 00000000..038e3a6b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/oracle.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "control-stale-target", + "expectedStatus": "declined", + "expectedDiagnostic": "unemittable-structured-control", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "A branch is redirected to a valid boundary that violates the structured-control premise.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/IfStatements.test.js:4-115", + "encoder/js-confuser-vm/test/features/Loops.test.js:4-186" + ], + "semanticTags": [ + "structured-control", + "stale-target", + "parseable-reference" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/source.js new file mode 100644 index 00000000..e74b28cf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/source.js @@ -0,0 +1,3 @@ +var x = 2; +if (x > 1) { x = x + 1; } else { x = 0; } +window.TEST_OUTPUT = x; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/encoded.js new file mode 100644 index 00000000..10fb31b4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */3, /* 3 */undefined]; +var BYTECODE = [4,1,2,2,0,0,0,3,1,0,0,4,2,0,9,2,3,4,0,2,3,0,45,2,0]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 5; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/oracle.json new file mode 100644 index 00000000..35577c9c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "extra-trailing-word", + "expectedStatus": "declined", + "expectedDiagnostic": "wordcode-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "An additional trailing word is not a complete instruction and is rejected.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/Literals.test.js:4-177" + ], + "semanticTags": [ + "numeric-wordcode", + "trailing-word", + "atomic-decline" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/source.js new file mode 100644 index 00000000..6d3a42f3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/extra-trailing-word/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = 3; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/encoded.js new file mode 100644 index 00000000..206148ef --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */undefined, /* 3 */"push", /* 4 */"try", /* 5 */1, /* 6 */"finally"]; +var BYTECODE = [4,1,47,4,47,0,8,1,0,1,2,5,3,4,48,4,0,5,2,4,2,4,0,0,0,5,1,0,42,6,3,0,48,7,2,6,2,9,4,5,7,0,4,2,0,45,4,4,1,59,94,2,3,0,3,4,0,0,5,3,0,8,6,4,5,0,5,4,0,43,7,4,6,1,5,0,4,5,0,5,3,4,1,2,119,55,39,94,55,1,2,121,39,94,3,4,0,0,5,3,0,8,6,4,5,0,5,6,0,43,7,4,6,1,5,58,2,46,3,45,3,0,4,2,0,45,4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/oracle.json new file mode 100644 index 00000000..d389084d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "finally-stale-target", + "expectedStatus": "declined", + "expectedDiagnostic": "functions-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "The FINALLY_SETUP throw-pad target is redirected into the wrong function region.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/TryFinally.test.js:1-402" + ], + "semanticTags": [ + "finally-handler", + "stale-throwpad", + "function-partition" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/source.js new file mode 100644 index 00000000..b4274f74 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/source.js @@ -0,0 +1,3 @@ +var log = []; +function run() { try { log.push("try"); return 1; } finally { log.push("finally"); } } +window.TEST_OUTPUT = [run(), log]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/encoded.js new file mode 100644 index 00000000..8658bbd9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */undefined, /* 3 */"push", /* 4 */"try", /* 5 */1, /* 6 */"finally"]; +var BYTECODE = [4,1,47,4,47,0,8,1,0,1,2,5,3,4,48,4,0,5,2,4,2,4,0,0,0,5,1,0,42,6,3,0,48,7,2,6,2,9,4,5,7,0,4,2,0,45,4,4,1,59,4294967295,2,3,117,3,4,0,0,5,3,0,8,6,4,5,0,5,4,0,43,7,4,6,1,5,0,4,5,0,5,3,4,1,2,119,55,39,94,55,1,2,121,39,94,3,4,0,0,5,3,0,8,6,4,5,0,5,6,0,43,7,4,6,1,5,58,2,46,3,45,3,0,4,2,0,45,4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/oracle.json new file mode 100644 index 00000000..ac065f91 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "handler-invalid-target", + "expectedStatus": "declined", + "expectedDiagnostic": "references-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "The FINALLY_SETUP handler target is outside the wordcode stream.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/TryFinally.test.js:1-402" + ], + "semanticTags": [ + "finally-handler", + "target-reference", + "out-of-range" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/source.js new file mode 100644 index 00000000..b4274f74 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/handler-invalid-target/source.js @@ -0,0 +1,3 @@ +var log = []; +function run() { try { log.push("try"); return 1; } finally { log.push("finally"); } } +window.TEST_OUTPUT = [run(), log]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/encoded.js new file mode 100644 index 00000000..1820f7dc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */3, /* 3 */undefined]; +var BYTECODE = []; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 5; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/oracle.json new file mode 100644 index 00000000..bf6cf299 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "malformed-container-empty-wordcode", + "expectedStatus": "declined", + "expectedDiagnostic": "container-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "The VM roles are present but the bytecode stream is empty, which violates the container contract before wordcode decoding.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/Literals.test.js:4-177" + ], + "semanticTags": [ + "parseable-container", + "malformed-wordcode", + "empty-stream" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/source.js new file mode 100644 index 00000000..6d3a42f3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/malformed-container-empty-wordcode/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = 3; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/encoded.js new file mode 100644 index 00000000..6a9668cc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */3, /* 3 */undefined]; +var NOT_BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 9, 2, 3, 4, 0, 2, 3, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 5; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/oracle.json new file mode 100644 index 00000000..f4f697c2 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "missing-container-role", + "expectedStatus": "declined", + "expectedDiagnostic": "container-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "The bytecode role is renamed, so the source is not a valid container.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/Literals.test.js:4-177" + ], + "semanticTags": [ + "parseable-source", + "missing-bytecode-role", + "container-contract" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/source.js new file mode 100644 index 00000000..6d3a42f3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/missing-container-role/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = 3; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/encoded.js new file mode 100644 index 00000000..c596cf89 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/encoded.js @@ -0,0 +1,2 @@ +const answer = 42; +window.TEST_OUTPUT = answer; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/oracle.json new file mode 100644 index 00000000..2fd15211 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "ordinary-near-miss", + "expectedStatus": "declined", + "expectedDiagnostic": "container-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "Parseable ordinary JavaScript has no jsconfuser-vm container role.", + "sourceEvidence": [ + "decoder/decode-js/test/vm/jsconfuser-vm/integration/harness.test.js:167-179" + ], + "semanticTags": [ + "ordinary-javascript", + "parseable", + "not-a-vm-container" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/source.js new file mode 100644 index 00000000..c596cf89 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/ordinary-near-miss/source.js @@ -0,0 +1,2 @@ +const answer = 42; +window.TEST_OUTPUT = answer; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/encoded.js new file mode 100644 index 00000000..00111f30 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */3, /* 3 */undefined]; +var BYTECODE = [4,1,2,2,0,0,0,3,4294967295,0,0,4,2,0,9,2,3,4,0,2,3,0,45,2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 5; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/oracle.json new file mode 100644 index 00000000..60f23b7a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "reference-invalid-constant", + "expectedStatus": "declined", + "expectedDiagnostic": "references-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "A LOAD_CONST constant-table index is outside the container's constant table.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/Literals.test.js:4-177" + ], + "semanticTags": [ + "constant-reference", + "out-of-range", + "atomic-decline" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/source.js new file mode 100644 index 00000000..6d3a42f3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-constant/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = 3; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/encoded.js new file mode 100644 index 00000000..682b615c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */2, /* 1 */1, /* 2 */0, /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */undefined]; +var BYTECODE = [4,1,0,3,0,0,5,2,3,0,3,1,0,23,4,2,3,40,4,4294967295,0,3,1,0,11,4,2,3,5,2,4,39,40,0,3,2,0,5,2,3,2,3,3,0,0,4,4,0,9,3,4,2,0,3,5,0,45,3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 5; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/oracle.json new file mode 100644 index 00000000..cdb0cf52 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/oracle.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "reference-invalid-label", + "expectedStatus": "declined", + "expectedDiagnostic": "references-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "A conditional branch target is changed to an out-of-range wordcode address.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/IfStatements.test.js:4-115", + "encoder/js-confuser-vm/test/features/Loops.test.js:4-186" + ], + "semanticTags": [ + "label-reference", + "out-of-range", + "branch-operand" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/source.js new file mode 100644 index 00000000..e74b28cf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-label/source.js @@ -0,0 +1,3 @@ +var x = 2; +if (x > 1) { x = x + 1; } else { x = 0; } +window.TEST_OUTPUT = x; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/encoded.js new file mode 100644 index 00000000..e915f361 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */2, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */1, /* 4 */undefined]; +var BYTECODE = [4,1,47,4,0,1,5,0,0,5,2,4,0,4,0,0,42,5,2,1,4,5,3,5,2,4,1,0,0,5,2,0,0,6,3,0,42,7,3,1,6,0,6,0,0,42,8,3,1,6,48,6,2,7,8,9,4,5,6,0,4,4,0,45,4,4,2,5,3,0,47,4,87,1,5,1,0,1,3,45,4,0,4,4,0,45,4,4,2,3,3,0,11,4,3,0,7,0,4,3,3,0,45,3,0,3,4,0,45,3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/oracle.json new file mode 100644 index 00000000..3beb8e1a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "reference-invalid-ownership", + "expectedStatus": "declined", + "expectedDiagnostic": "functions-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "A MAKE_CLOSURE target is redirected to the root function, breaking ownership partitioning.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/Closures.test.js:4-110" + ], + "semanticTags": [ + "closure-reference", + "function-ownership", + "invalid-target" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/source.js new file mode 100644 index 00000000..1a18c894 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/reference-invalid-ownership/source.js @@ -0,0 +1,3 @@ +function make(seed) { var total = seed; return function (step) { total += step; return total; }; } +var next = make(2); +window.TEST_OUTPUT = [next(1), next(2)]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/encoded.js new file mode 100644 index 00000000..4e4e915e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */3, /* 3 */undefined]; +var BYTECODE = [4,1,2,2,0,0,0,3,1,0,0,4,2,0,9,2,3,4,0,2,3,0,45]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 5; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/oracle.json new file mode 100644 index 00000000..e5f2d985 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "truncated-wordcode", + "expectedStatus": "declined", + "expectedDiagnostic": "wordcode-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "The final instruction is removed, leaving a structurally truncated stream.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/Literals.test.js:4-177" + ], + "semanticTags": [ + "numeric-wordcode", + "truncated-instruction", + "atomic-decline" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/source.js new file mode 100644 index 00000000..6d3a42f3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/truncated-wordcode/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = 3; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/encoded.js new file mode 100644 index 00000000..f2223716 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */3, /* 3 */undefined]; +var BYTECODE = [4294967295,1,2,2,0,0,0,3,1,0,0,4,2,0,9,2,3,4,0,2,3,0,45,2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 5; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/oracle.json new file mode 100644 index 00000000..0114644e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/oracle.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": "negative-oracle.v1", + "cellId": "unknown-opcode", + "expectedStatus": "declined", + "expectedDiagnostic": "wordcode-declined", + "outputMustEqualInput": true, + "targetExecution": false, + "vmExecuted": false, + "behavioralOracleInvoked": false, + "reason": "The first opcode is outside the pinned VM opcode table.", + "sourceEvidence": [ + "encoder/js-confuser-vm/test/features/Literals.test.js:4-177" + ], + "semanticTags": [ + "numeric-wordcode", + "unknown-opcode", + "atomic-decline" + ], + "comparison": { + "sourceExpected": "static-decline-contract", + "recoveredExpected": "not-applicable" + }, + "exclusions": [ + "This negative oracle is never evaled or executed; it records only the registered decline contract.", + "No behavioral source oracle is used to make a decline look like successful decompilation." + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/source.js new file mode 100644 index 00000000..6d3a42f3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/negative/unknown-opcode/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = 3; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/encoded.js new file mode 100644 index 00000000..7f644743 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */1, /* 1 */"computed", /* 2 */"value", /* 3 */"method", /* 4 */11, /* 5 */7, /* 6 */"window", /* 7 */"TEST_OUTPUT", /* 8 */"Object", /* 9 */"keys", /* 10 */"sort", /* 11 */undefined]; +var BYTECODE = [4, 1, 0, 5, 0, 0, 5, 2, 5, 0, 5, 1, 0, 5, 3, 5, 49, 5, 0, 0, 6, 2, 0, 47, 7, 172, 0, 3, 1, 0, 1, 2, 50, 5, 6, 7, 0, 6, 2, 0, 47, 7, 185, 1, 4, 1, 0, 1, 2, 51, 5, 6, 7, 0, 6, 3, 0, 47, 7, 196, 0, 4, 0, 0, 9, 5, 6, 7, 0, 6, 4, 0, 9, 5, 3, 6, 5, 4, 5, 0, 5, 2, 0, 0, 6, 5, 0, 9, 4, 5, 6, 2, 5, 6, 0, 0, 6, 7, 0, 0, 7, 2, 0, 8, 8, 4, 7, 0, 7, 3, 0, 8, 9, 4, 7, 43, 7, 4, 9, 0, 8, 9, 4, 3, 2, 10, 8, 0, 0, 11, 9, 0, 8, 12, 10, 11, 43, 11, 10, 12, 1, 4, 0, 10, 10, 0, 8, 12, 11, 10, 43, 10, 11, 12, 0, 48, 11, 4, 8, 7, 9, 10, 9, 5, 6, 11, 0, 5, 11, 0, 45, 5, 4, 1, 3, 2, 0, 45, 2, 0, 2, 11, 0, 45, 2, 4, 2, 7, 0, 0, 0, 3, 11, 0, 45, 3, 4, 1, 0, 2, 2, 0, 8, 3, 1, 2, 45, 3, 0, 2, 11, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 13; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/oracle.json new file mode 100644 index 00000000..87dd10bf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/oracle.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-accessors-and-methods", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 7, + 7, + 11, + [ + "computed", + "method", + "value" + ] + ], + "expectedThrow": null, + "semanticTags": [ + "getter", + "setter", + "computed-key", + "computed-state", + "method-shorthand", + "enumeration" + ], + "hazards": [ + "Computed key evaluation and accessor/method enumeration are observed through explicit values and sorted keys; property identity is not compared across processes.", + "The child uses the minimal-window-v1 profile and no ambient globals beyond the declared source." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/source.js new file mode 100644 index 00000000..6db7b240 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-accessors-and-methods/source.js @@ -0,0 +1,5 @@ +var backing = 1; +var computedKey = "computed"; +var object = { get value() { return backing; }, set value(next) { backing = next; }, method() { return this.value; }, [computedKey]: 11 }; +object.value = 7; +window.TEST_OUTPUT = [object.value, object.method(), object[computedKey], Object.keys(object).sort()]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/encoded.js new file mode 100644 index 00000000..8657da0a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */1, /* 1 */2, /* 2 */3, /* 3 */"map", /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */undefined, /* 7 */"x", /* 8 */"y"]; +var BYTECODE = [4, 1, 47, 5, 106, 2, 3, 0, 0, 5, 2, 5, 47, 5, 112, 0, 5, 0, 0, 42, 6, 5, 0, 5, 3, 6, 0, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 48, 8, 3, 5, 6, 7, 0, 5, 3, 0, 8, 6, 8, 5, 47, 5, 137, 1, 3, 0, 0, 43, 7, 8, 6, 1, 5, 5, 4, 7, 2, 5, 4, 0, 0, 6, 5, 0, 0, 7, 1, 0, 0, 8, 2, 0, 42, 9, 2, 2, 7, 8, 48, 7, 3, 9, 3, 4, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 11, 2, 0, 1, 45, 2, 0, 0, 7, 0, 0, 1, 0, 0, 0, 2, 8, 0, 0, 3, 1, 0, 49, 4, 2, 0, 1, 2, 3, 45, 4, 0, 1, 1, 0, 13, 2, 0, 1, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/oracle.json new file mode 100644 index 00000000..454cd1c8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/oracle.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-arrow-forms", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 5, + { + "x": 1, + "y": 2 + }, + [ + 2, + 4, + 6 + ] + ], + "expectedThrow": null, + "semanticTags": [ + "arrow", + "concise-body", + "object-return", + "higher-order" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/source.js new file mode 100644 index 00000000..aa844abe --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/source.js @@ -0,0 +1,4 @@ +var add = (a, b) => a + b; +var object = (() => ({ x: 1, y: 2 }))(); +var doubled = [1, 2, 3].map((value) => value * 2); +window.TEST_OUTPUT = [add(2, 3), object, doubled]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/encoded.js new file mode 100644 index 00000000..65758bdc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"base", /* 1 */10, /* 2 */"make", /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */"window", /* 7 */"TEST_OUTPUT", /* 8 */"call", /* 9 */100, /* 10 */5, /* 11 */null, /* 12 */undefined, /* 13 */0]; +var BYTECODE = [4, 1, 0, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 47, 8, 156, 0, 3, 0, 0, 49, 9, 2, 5, 6, 7, 8, 5, 2, 9, 0, 5, 2, 0, 8, 6, 2, 5, 0, 5, 3, 0, 0, 7, 4, 0, 43, 8, 2, 6, 2, 5, 7, 5, 3, 8, 0, 5, 2, 0, 8, 6, 2, 5, 0, 5, 5, 0, 43, 7, 2, 6, 1, 5, 5, 4, 7, 2, 5, 6, 0, 0, 6, 7, 0, 0, 7, 8, 0, 8, 8, 3, 7, 0, 7, 0, 0, 0, 9, 9, 0, 49, 10, 1, 7, 9, 0, 7, 10, 0, 43, 9, 3, 8, 2, 10, 7, 0, 7, 8, 0, 8, 8, 4, 7, 0, 7, 11, 0, 0, 10, 4, 0, 43, 11, 4, 8, 2, 7, 10, 48, 7, 2, 9, 11, 9, 5, 6, 7, 0, 5, 12, 0, 45, 5, 4, 1, 47, 2, 177, 1, 5, 2, 0, 1, 1, 1, 0, 45, 2, 0, 2, 12, 0, 45, 2, 3, 1, 0, 0, 2, 0, 0, 8, 3, 1, 2, 11, 1, 3, 0, 3, 2, 1, 0, 3, 13, 0, 8, 4, 2, 3, 11, 2, 1, 4, 3, 1, 1, 0, 3, 3, 0, 8, 4, 1, 3, 5, 1, 4, 41, 1, 231, 0, 3, 13, 0, 5, 1, 3, 11, 3, 2, 1, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/oracle.json new file mode 100644 index 00000000..872a166e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/oracle.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-arrow-lexical-state", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 18, + 15 + ], + "expectedThrow": null, + "semanticTags": [ + "arrow", + "lexical-this", + "lexical-arguments", + "closure", + "receiver" + ], + "hazards": [ + "The arrow must retain both the method receiver and enclosing arguments despite explicit call/apply receivers; only numeric results are compared.", + "Missing arguments[1] is deliberately normalized through the source's own fallback, not by the oracle." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/source.js new file mode 100644 index 00000000..280c43f3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-lexical-state/source.js @@ -0,0 +1,4 @@ +var object = { base: 10, make: function () { return (value) => this.base + value + arguments[0] + (arguments[1] || 0); } }; +var first = object.make(1, 2); +var second = object.make(3); +window.TEST_OUTPUT = [first.call({ base: 100 }, 5), second.call(null, 2)]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/encoded.js new file mode 100644 index 00000000..8ef12b3e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */3, /* 1 */undefined, /* 2 */4, /* 3 */"high", /* 4 */2, /* 5 */"mid", /* 6 */"low", /* 7 */1, /* 8 */"odd", /* 9 */"even", /* 10 */"window", /* 11 */"TEST_OUTPUT"]; +var BYTECODE = [4, 1, 0, 5, 0, 0, 5, 2, 5, 0, 3, 1, 0, 0, 5, 2, 0, 23, 6, 2, 5, 40, 6, 33, 0, 5, 3, 0, 5, 3, 5, 39, 60, 0, 5, 4, 0, 23, 6, 2, 5, 40, 6, 53, 0, 5, 5, 0, 5, 3, 5, 39, 60, 0, 5, 6, 0, 5, 3, 5, 0, 5, 4, 0, 15, 6, 2, 5, 0, 5, 7, 0, 26, 7, 6, 5, 40, 7, 88, 0, 5, 8, 0, 5, 6, 5, 39, 95, 0, 5, 9, 0, 5, 6, 5, 5, 4, 6, 2, 5, 10, 0, 0, 6, 11, 0, 48, 7, 2, 3, 4, 9, 5, 6, 7, 0, 5, 1, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/oracle.json new file mode 100644 index 00000000..c60da2b4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/oracle.json @@ -0,0 +1,34 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-branching", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + "mid", + "odd" + ], + "expectedThrow": null, + "semanticTags": [ + "if", + "else-if", + "nested-branch", + "conditional-expression" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/source.js new file mode 100644 index 00000000..8fa189a8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-branching/source.js @@ -0,0 +1,5 @@ +var n = 3; +var branch; +if (n > 4) branch = "high"; else if (n > 2) branch = "mid"; else branch = "low"; +var nested = n % 2 === 1 ? "odd" : "even"; +window.TEST_OUTPUT = [branch, nested]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/encoded.js new file mode 100644 index 00000000..a4b112bd --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */undefined, /* 3 */0, /* 4 */1]; +var BYTECODE = [4, 1, 47, 4, 55, 0, 4, 0, 0, 5, 2, 4, 42, 4, 2, 0, 5, 3, 4, 2, 4, 0, 0, 0, 5, 1, 0, 42, 6, 3, 0, 42, 7, 3, 0, 42, 8, 3, 0, 48, 9, 3, 6, 7, 8, 9, 4, 5, 9, 0, 4, 2, 0, 45, 4, 4, 1, 0, 3, 3, 0, 5, 2, 3, 47, 3, 81, 0, 5, 1, 0, 1, 2, 45, 3, 0, 3, 2, 0, 45, 3, 4, 1, 3, 2, 0, 5, 3, 2, 0, 3, 4, 0, 11, 4, 2, 3, 7, 0, 4, 3, 2, 0, 45, 2, 0, 2, 2, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/oracle.json new file mode 100644 index 00000000..ec025de4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/oracle.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-closures-state", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 1, + 2, + 3 + ], + "expectedThrow": null, + "semanticTags": [ + "closure", + "upvalue", + "mutable-state", + "repeated-call" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/source.js new file mode 100644 index 00000000..49c89bfd --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/source.js @@ -0,0 +1,3 @@ +function factory() { var count = 0; return function () { count++; return count; }; } +var next = factory(); +window.TEST_OUTPUT = [next(), next(), next()]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/encoded.js new file mode 100644 index 00000000..c5667377 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"push", /* 1 */"before", /* 2 */"after", /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */undefined]; +var BYTECODE = [4, 1, 48, 3, 0, 5, 2, 3, 0, 3, 0, 0, 8, 4, 2, 3, 0, 3, 1, 0, 43, 5, 2, 4, 1, 3, 57, 0, 3, 0, 0, 8, 4, 2, 3, 0, 3, 2, 0, 43, 5, 2, 4, 1, 3, 2, 3, 3, 0, 0, 4, 4, 0, 9, 3, 4, 2, 0, 3, 5, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 6; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/oracle.json new file mode 100644 index 00000000..42fda81a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/oracle.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-debugger-order", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + "before", + "after" + ], + "expectedThrow": null, + "semanticTags": [ + "debugger-statement", + "statement-order" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/source.js new file mode 100644 index 00000000..2a2d391e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/source.js @@ -0,0 +1,5 @@ +var log = []; +log.push("before"); +debugger; +log.push("after"); +window.TEST_OUTPUT = log; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/encoded.js new file mode 100644 index 00000000..350f7a1d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */1, /* 1 */"x", /* 2 */10, /* 3 */5, /* 4 */"missing", /* 5 */3, /* 6 */2, /* 7 */"window", /* 8 */"TEST_OUTPUT", /* 9 */"result", /* 10 */"object", /* 11 */undefined]; +var BYTECODE = [4, 1, 0, 5, 0, 0, 5, 2, 5, 0, 5, 1, 0, 0, 6, 2, 0, 49, 7, 1, 5, 6, 5, 3, 7, 5, 5, 2, 0, 6, 0, 0, 11, 7, 2, 6, 5, 2, 7, 0, 6, 0, 0, 11, 7, 2, 6, 5, 2, 7, 0, 6, 1, 0, 8, 8, 3, 6, 0, 9, 3, 0, 11, 10, 8, 9, 9, 3, 6, 10, 0, 6, 1, 0, 8, 8, 3, 6, 5, 9, 8, 0, 11, 0, 0, 12, 12, 8, 11, 9, 3, 6, 12, 0, 6, 4, 0, 10, 8, 3, 6, 0, 6, 5, 0, 5, 2, 6, 0, 6, 6, 0, 13, 11, 2, 6, 48, 6, 6, 5, 7, 10, 9, 8, 11, 5, 4, 6, 2, 5, 7, 0, 0, 6, 8, 0, 0, 7, 9, 0, 0, 8, 1, 0, 0, 9, 10, 0, 49, 10, 3, 7, 4, 8, 2, 9, 3, 9, 5, 6, 10, 0, 5, 11, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 13; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/oracle.json new file mode 100644 index 00000000..715a1fff --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/oracle.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-expression-mutation", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": { + "result": [ + 1, + 3, + 15, + 15, + true, + 6 + ], + "x": 3, + "object": { + "x": 14 + } + }, + "expectedThrow": null, + "semanticTags": [ + "assignment", + "mutation", + "update", + "delete", + "sequence" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/source.js new file mode 100644 index 00000000..7fae670f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-mutation/source.js @@ -0,0 +1,4 @@ +var x = 1; +var object = { x: 10 }; +var result = [x++, ++x, object.x += 5, object.x--, delete object.missing, (x = 3, x * 2)]; +window.TEST_OUTPUT = { result: result, x: x, object: object }; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/encoded.js new file mode 100644 index 00000000..e9040525 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */2, /* 3 */3, /* 4 */5, /* 5 */8, /* 6 */4, /* 7 */"x", /* 8 */1, /* 9 */"Array", /* 10 */"undefined", /* 11 */0, /* 12 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 11, 6, 4, 5, 0, 4, 2, 0, 0, 5, 3, 0, 0, 7, 2, 0, 60, 8, 5, 7, 60, 5, 4, 8, 0, 4, 4, 0, 0, 7, 3, 0, 16, 8, 4, 7, 0, 4, 5, 0, 17, 7, 8, 4, 0, 4, 3, 0, 0, 8, 6, 0, 22, 9, 4, 8, 0, 4, 7, 0, 0, 8, 7, 0, 0, 10, 8, 0, 49, 11, 1, 8, 10, 30, 8, 4, 11, 48, 4, 0, 2, 10, 9, 0, 31, 11, 4, 10, 38, 4, 10, 0, 0, 10, 11, 0, 37, 12, 10, 48, 10, 8, 6, 5, 7, 9, 8, 11, 4, 12, 9, 2, 3, 10, 0, 2, 12, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 13; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/oracle.json new file mode 100644 index 00000000..a7c64d0e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/oracle.json @@ -0,0 +1,43 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-expression-values", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 5, + 512, + 9, + true, + true, + true, + "undefined", + { + "type": "undefined" + } + ], + "expectedThrow": null, + "semanticTags": [ + "operators", + "precedence", + "instanceof", + "typeof", + "void" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/source.js new file mode 100644 index 00000000..a36d0c1d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/source.js @@ -0,0 +1,4 @@ +window.TEST_OUTPUT = [ + 2 + 3, 2 ** 3 ** 2, (5 & 3) | 8, 3 < 4, + "x" in { x: 1 }, [] instanceof Array, typeof undefined, void 0, +]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/encoded.js new file mode 100644 index 00000000..56dbd118 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */undefined, /* 3 */"push", /* 4 */"try", /* 5 */"normal", /* 6 */"finally", /* 7 */"boom", /* 8 */"finally-catch", /* 9 */"deep", /* 10 */"inner-finally", /* 11 */"nested-caught-", /* 12 */"outer-finally", /* 13 */0, /* 14 */3, /* 15 */1, /* 16 */2, /* 17 */"loop-body-", /* 18 */"loop-finally-"]; +var BYTECODE = [4, 1, 47, 9, 102, 0, 8, 1, 0, 1, 2, 5, 3, 9, 47, 9, 182, 0, 9, 1, 0, 1, 2, 5, 4, 9, 47, 9, 255, 0, 11, 1, 0, 1, 2, 5, 5, 9, 47, 9, 372, 0, 9, 1, 0, 1, 2, 5, 6, 9, 48, 9, 0, 5, 2, 9, 42, 9, 3, 0, 5, 7, 9, 42, 9, 4, 0, 5, 8, 9, 42, 9, 5, 0, 42, 9, 6, 0, 2, 9, 0, 0, 0, 10, 1, 0, 48, 11, 3, 7, 8, 2, 9, 9, 10, 11, 0, 9, 2, 0, 45, 9, 4, 1, 59, 149, 2, 3, 172, 3, 4, 0, 0, 5, 3, 0, 8, 6, 4, 5, 0, 5, 4, 0, 43, 7, 4, 6, 1, 5, 0, 4, 5, 0, 5, 3, 4, 1, 2, 174, 55, 39, 149, 55, 1, 2, 176, 39, 149, 3, 4, 0, 0, 5, 3, 0, 8, 6, 4, 5, 0, 5, 6, 0, 43, 7, 4, 6, 1, 5, 58, 2, 46, 3, 45, 3, 0, 4, 2, 0, 45, 4, 4, 1, 59, 224, 3, 4, 247, 54, 201, 2, 0, 5, 7, 0, 46, 5, 55, 39, 218, 3, 5, 0, 0, 6, 3, 0, 8, 7, 5, 6, 43, 6, 5, 7, 1, 2, 55, 1, 3, 249, 39, 224, 3, 5, 0, 0, 6, 3, 0, 8, 7, 5, 6, 0, 6, 8, 0, 43, 8, 5, 7, 1, 6, 58, 3, 46, 4, 0, 5, 2, 0, 45, 5, 4, 1, 59, 341, 3, 4, 364, 54, 310, 2, 59, 282, 5, 6, 305, 0, 7, 9, 0, 46, 7, 55, 1, 5, 307, 39, 282, 3, 7, 0, 0, 8, 3, 0, 8, 9, 7, 8, 0, 8, 10, 0, 43, 10, 7, 9, 1, 8, 58, 5, 46, 6, 55, 39, 335, 3, 7, 0, 0, 8, 3, 0, 8, 9, 7, 8, 0, 8, 11, 0, 11, 10, 8, 2, 43, 8, 7, 9, 1, 10, 55, 1, 3, 366, 39, 341, 3, 7, 0, 0, 8, 3, 0, 8, 9, 7, 8, 0, 8, 12, 0, 43, 10, 7, 9, 1, 8, 58, 3, 46, 4, 0, 7, 2, 0, 45, 7, 4, 1, 0, 5, 13, 0, 5, 2, 5, 0, 5, 14, 0, 22, 6, 2, 5, 40, 6, 511, 59, 462, 3, 4, 489, 0, 5, 15, 0, 26, 6, 2, 5, 40, 6, 414, 1, 3, 491, 55, 39, 462, 0, 5, 16, 0, 26, 6, 2, 5, 40, 6, 431, 1, 3, 493, 55, 39, 462, 3, 5, 0, 0, 6, 3, 0, 8, 7, 5, 6, 0, 6, 17, 0, 11, 8, 6, 2, 43, 6, 5, 7, 1, 8, 55, 1, 3, 495, 39, 462, 3, 5, 0, 0, 6, 3, 0, 8, 7, 5, 6, 0, 6, 18, 0, 11, 8, 6, 2, 43, 6, 5, 7, 1, 8, 58, 3, 46, 4, 39, 495, 39, 511, 5, 5, 2, 0, 5, 15, 0, 11, 6, 2, 5, 5, 2, 6, 39, 381, 0, 5, 2, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/oracle.json new file mode 100644 index 00000000..b73a1d5f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/oracle.json @@ -0,0 +1,52 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-finally-abrupt", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + "normal", + { + "type": "undefined" + }, + [ + "try", + "finally", + "boom", + "finally-catch", + "inner-finally", + "nested-caught-deep", + "outer-finally", + "loop-body-0", + "loop-finally-0", + "loop-finally-1", + "loop-finally-2" + ] + ], + "expectedThrow": null, + "semanticTags": [ + "finally", + "return", + "throw", + "catch", + "nested-finally", + "loop-abrupt", + "completion-order" + ], + "hazards": [ + "The log is the source-visible oracle for nested-finalizer order and for finalizers crossed by continue/break; exception stacks are excluded.", + "The loop bounds are fixed and synchronous so every abrupt completion remains finite and reproducible." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/source.js new file mode 100644 index 00000000..62fa58b8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/source.js @@ -0,0 +1,7 @@ +var log = []; +function normal() { try { log.push("try"); return "normal"; } finally { log.push("finally"); } } +function caught() { try { throw "boom"; } catch (error) { log.push(error); } finally { log.push("finally-catch"); } } +function nested() { try { try { throw "deep"; } finally { log.push("inner-finally"); } } catch (error) { log.push("nested-caught-" + error); } finally { log.push("outer-finally"); } } +function loopAbrupt() { for (var i = 0; i < 3; i++) { try { if (i === 1) continue; if (i === 2) break; log.push("loop-body-" + i); } finally { log.push("loop-finally-" + i); } } } +var first = normal(); var second = caught(); nested(); loopAbrupt(); +window.TEST_OUTPUT = [first, second, log]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/encoded.js new file mode 100644 index 00000000..610767eb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"prototype", /* 1 */"inherited", /* 2 */1, /* 3 */"own", /* 4 */2, /* 5 */"push", /* 6 */"window", /* 7 */"TEST_OUTPUT", /* 8 */"raw", /* 9 */"sorted", /* 10 */"slice", /* 11 */"sort", /* 12 */undefined]; +var BYTECODE = [4, 1, 47, 7, 142, 0, 3, 0, 0, 5, 2, 7, 0, 7, 0, 0, 8, 8, 2, 7, 0, 7, 1, 0, 0, 9, 2, 0, 9, 8, 7, 9, 44, 7, 2, 0, 5, 3, 7, 0, 7, 3, 0, 0, 8, 4, 0, 9, 3, 7, 8, 48, 7, 0, 5, 4, 7, 52, 5, 3, 53, 7, 5, 83, 5, 6, 7, 0, 7, 5, 0, 8, 8, 4, 7, 43, 7, 4, 8, 1, 6, 39, 60, 2, 7, 6, 0, 0, 8, 7, 0, 0, 9, 8, 0, 0, 10, 9, 0, 0, 11, 10, 0, 8, 12, 4, 11, 43, 11, 4, 12, 0, 0, 12, 11, 0, 8, 13, 11, 12, 43, 12, 11, 13, 0, 49, 11, 2, 9, 4, 10, 12, 9, 7, 8, 11, 0, 7, 12, 0, 45, 7, 4, 1, 0, 2, 12, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 14; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/oracle.json new file mode 100644 index 00000000..23279fa6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/oracle.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-for-in-enumeration", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": { + "raw": [ + "own", + "inherited" + ], + "sorted": [ + "inherited", + "own" + ] + }, + "expectedThrow": null, + "semanticTags": [ + "for-in", + "enumeration", + "prototype", + "own-properties" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/source.js new file mode 100644 index 00000000..c4c5251e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/source.js @@ -0,0 +1,5 @@ +function Parent() {} +Parent.prototype.inherited = 1; +var object = new Parent(); object.own = 2; +var keys = []; for (var key in object) keys.push(key); +window.TEST_OUTPUT = { raw: keys, sorted: keys.slice().sort() }; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/encoded.js new file mode 100644 index 00000000..f785ebc8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */2, /* 3 */3, /* 4 */4, /* 5 */"undefined", /* 6 */undefined, /* 7 */"hoisted", /* 8 */7]; +var BYTECODE = [4, 1, 47, 5, 112, 2, 5, 0, 0, 5, 2, 5, 47, 5, 126, 1, 5, 0, 0, 5, 3, 5, 47, 5, 144, 0, 3, 0, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 0, 8, 3, 0, 42, 9, 2, 2, 7, 8, 0, 7, 4, 0, 42, 8, 3, 1, 7, 47, 7, 158, 0, 3, 0, 0, 42, 10, 7, 0, 47, 7, 172, 1, 4, 0, 0, 2, 11, 5, 0, 42, 12, 7, 1, 11, 42, 7, 4, 0, 48, 11, 5, 9, 8, 10, 12, 7, 9, 5, 6, 11, 0, 5, 6, 0, 45, 5, 4, 3, 11, 4, 0, 1, 45, 4, 0, 4, 6, 0, 45, 4, 4, 2, 0, 3, 2, 0, 13, 4, 0, 3, 45, 4, 0, 3, 6, 0, 45, 3, 4, 1, 0, 2, 7, 0, 45, 2, 0, 2, 6, 0, 45, 2, 4, 1, 0, 2, 8, 0, 45, 2, 0, 2, 6, 0, 45, 2, 4, 2, 45, 0, 0, 3, 6, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 13; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/oracle.json new file mode 100644 index 00000000..e31128a4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/oracle.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-functions-call-return", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 5, + 8, + 7, + { + "type": "undefined" + }, + "hoisted" + ], + "expectedThrow": null, + "semanticTags": [ + "function", + "call", + "return", + "iife", + "hoisting" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/source.js new file mode 100644 index 00000000..f24d5403 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-call-return/source.js @@ -0,0 +1,4 @@ +function add(a, b) { return a + b; } +function double(value) { return value * 2; } +function before() { return "hoisted"; } +window.TEST_OUTPUT = [add(2, 3), double(4), (function () { return 7; })(), (function (value) { return value; })(undefined), before()]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/encoded.js new file mode 100644 index 00000000..7516c79b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"Ada", /* 3 */1, /* 4 */2, /* 5 */undefined, /* 6 */"World", /* 7 */"Hello, ", /* 8 */"!", /* 9 */"length"]; +var BYTECODE = [4, 1, 47, 4, 83, 1, 6, 0, 0, 5, 2, 4, 47, 4, 127, 2, 6, 0, 0, 5, 3, 4, 2, 4, 0, 0, 0, 5, 1, 0, 42, 6, 2, 0, 0, 7, 2, 0, 42, 8, 2, 1, 7, 0, 7, 3, 0, 0, 9, 4, 0, 42, 10, 3, 2, 7, 9, 0, 7, 3, 0, 42, 9, 3, 1, 7, 48, 7, 4, 6, 8, 10, 9, 9, 4, 5, 7, 0, 4, 5, 0, 45, 4, 4, 2, 0, 3, 5, 0, 26, 4, 0, 3, 40, 4, 103, 0, 3, 6, 0, 5, 0, 3, 0, 3, 7, 0, 11, 4, 3, 0, 0, 3, 8, 0, 11, 5, 4, 3, 45, 5, 0, 3, 5, 0, 45, 3, 4, 3, 0, 4, 9, 0, 8, 5, 2, 4, 48, 4, 3, 5, 0, 1, 45, 4, 0, 4, 5, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/oracle.json new file mode 100644 index 00000000..5d5e4a1d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/oracle.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-functions-params", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + "Hello, World!", + "Hello, Ada!", + [ + 2, + 1, + 2 + ], + [ + 1, + 1, + { + "type": "undefined" + } + ] + ], + "expectedThrow": null, + "semanticTags": [ + "default-parameters", + "arguments", + "arity" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/source.js new file mode 100644 index 00000000..5c5bf22b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-params/source.js @@ -0,0 +1,3 @@ +function greet(name = "World") { return "Hello, " + name + "!"; } +function collect(a, b) { return [arguments.length, a, b]; } +window.TEST_OUTPUT = [greet(), greet("Ada"), collect(1, 2), collect(1)]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/encoded.js new file mode 100644 index 00000000..5985b2a3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */2, /* 4 */3, /* 5 */4, /* 6 */5, /* 7 */10, /* 8 */20, /* 9 */30, /* 10 */undefined, /* 11 */"reduce", /* 12 */0, /* 13 */"length"]; +var BYTECODE = [4, 1, 47, 4, 103, 1, 7, 0, 1, 5, 2, 4, 47, 4, 153, 2, 8, 0, 1, 5, 3, 4, 2, 4, 0, 0, 0, 5, 1, 0, 0, 6, 2, 0, 0, 7, 3, 0, 0, 8, 4, 0, 0, 9, 5, 0, 0, 10, 6, 0, 42, 11, 2, 5, 6, 7, 8, 9, 10, 0, 6, 7, 0, 0, 7, 8, 0, 0, 8, 9, 0, 42, 9, 3, 3, 6, 7, 8, 0, 6, 7, 0, 42, 7, 3, 1, 6, 48, 6, 3, 11, 9, 7, 9, 4, 5, 6, 0, 4, 10, 0, 45, 4, 4, 2, 0, 3, 11, 0, 8, 4, 0, 3, 47, 3, 139, 2, 5, 0, 0, 0, 5, 12, 0, 43, 6, 0, 4, 2, 3, 5, 45, 6, 0, 3, 10, 0, 45, 3, 4, 3, 11, 4, 0, 1, 45, 4, 0, 4, 10, 0, 45, 4, 4, 3, 0, 4, 13, 0, 8, 5, 1, 4, 0, 4, 12, 0, 8, 6, 1, 4, 0, 4, 2, 0, 8, 7, 1, 4, 48, 4, 4, 0, 5, 6, 7, 45, 4, 0, 4, 10, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/oracle.json new file mode 100644 index 00000000..b059c9be --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/oracle.json @@ -0,0 +1,48 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-functions-rest", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 15, + [ + 10, + 2, + 20, + 30 + ], + [ + 10, + 0, + { + "type": "undefined" + }, + { + "type": "undefined" + } + ] + ], + "expectedThrow": null, + "semanticTags": [ + "rest-parameters", + "argument-packing", + "array" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/source.js new file mode 100644 index 00000000..461cc3bb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-functions-rest/source.js @@ -0,0 +1,3 @@ +function sum(...values) { return values.reduce(function (a, b) { return a + b; }, 0); } +function head(first, ...rest) { return [first, rest.length, rest[0], rest[1]]; } +window.TEST_OUTPUT = [sum(1, 2, 3, 4, 5), head(10, 20, 30), head(10)]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/encoded.js new file mode 100644 index 00000000..3900462d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */null, /* 1 */"definitely_missing_global", /* 2 */"name", /* 3 */"ReferenceError", /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */undefined]; +var BYTECODE = [4, 1, 0, 4, 0, 0, 5, 2, 4, 54, 19, 3, 2, 4, 1, 0, 55, 39, 43, 0, 4, 2, 0, 8, 5, 3, 4, 2, 4, 3, 0, 31, 6, 3, 4, 48, 4, 2, 5, 6, 5, 2, 4, 54, 53, 3, 2, 4, 1, 0, 55, 39, 53, 2, 4, 4, 0, 0, 5, 5, 0, 38, 6, 1, 0, 48, 7, 2, 2, 6, 9, 4, 5, 7, 0, 4, 6, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/oracle.json new file mode 100644 index 00000000..096feacf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/oracle.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-global-resolution", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + [ + "ReferenceError", + true + ], + "undefined" + ], + "expectedThrow": null, + "semanticTags": [ + "global-lookup", + "reference-error", + "typeof" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/source.js new file mode 100644 index 00000000..38c79ebc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-global-resolution/source.js @@ -0,0 +1,4 @@ +var missing = null; +try { definitely_missing_global; } catch (error) { missing = [error.name, error instanceof ReferenceError]; } +try { definitely_missing_global; } catch (error) {} +window.TEST_OUTPUT = [missing, typeof definitely_missing_global]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/encoded.js new file mode 100644 index 00000000..31156b8a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */null, /* 1 */"String", /* 2 */"name", /* 3 */"TypeError", /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */undefined]; +var BYTECODE = [4, 1, 0, 4, 0, 0, 5, 2, 4, 54, 25, 3, 48, 4, 0, 49, 5, 0, 31, 6, 4, 5, 55, 39, 58, 2, 4, 1, 0, 0, 5, 2, 0, 8, 6, 3, 5, 42, 5, 4, 1, 6, 2, 4, 3, 0, 31, 6, 3, 4, 48, 4, 2, 5, 6, 5, 2, 4, 2, 4, 4, 0, 0, 5, 5, 0, 9, 4, 5, 2, 0, 4, 6, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/oracle.json new file mode 100644 index 00000000..fe9fd104 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/oracle.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-instanceof-errors", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + "TypeError", + true + ], + "expectedThrow": null, + "semanticTags": [ + "instanceof", + "native-error", + "exception" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/source.js new file mode 100644 index 00000000..6654f7e0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-instanceof-errors/source.js @@ -0,0 +1,3 @@ +var caught = null; +try { [] instanceof {}; } catch (error) { caught = [String(error.name), error instanceof TypeError]; } +window.TEST_OUTPUT = caught; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/encoded.js new file mode 100644 index 00000000..95616ef4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */3, /* 2 */1, /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */undefined]; +var BYTECODE = [4, 1, 0, 8, 0, 0, 5, 2, 8, 0, 8, 0, 0, 5, 3, 8, 0, 8, 1, 0, 22, 9, 3, 8, 40, 9, 104, 0, 8, 0, 0, 5, 4, 8, 0, 8, 1, 0, 22, 9, 4, 8, 40, 9, 88, 5, 8, 2, 0, 8, 2, 0, 11, 9, 2, 8, 5, 2, 9, 0, 8, 2, 0, 26, 9, 4, 8, 40, 9, 72, 39, 104, 5, 8, 4, 0, 8, 2, 0, 11, 9, 4, 8, 5, 4, 9, 39, 34, 5, 8, 3, 0, 8, 2, 0, 11, 9, 3, 8, 5, 3, 9, 39, 16, 0, 8, 0, 0, 5, 5, 8, 0, 8, 0, 0, 5, 6, 8, 0, 8, 1, 0, 22, 9, 6, 8, 40, 9, 206, 0, 8, 0, 0, 5, 7, 8, 0, 8, 1, 0, 22, 9, 7, 8, 40, 9, 190, 0, 8, 2, 0, 26, 9, 7, 8, 40, 9, 160, 39, 190, 5, 8, 5, 0, 8, 2, 0, 11, 9, 5, 8, 5, 5, 9, 5, 8, 7, 0, 8, 2, 0, 11, 9, 7, 8, 5, 7, 9, 39, 136, 5, 8, 6, 0, 8, 2, 0, 11, 9, 6, 8, 5, 6, 9, 39, 118, 2, 8, 3, 0, 0, 9, 4, 0, 48, 10, 2, 2, 5, 9, 8, 9, 10, 0, 8, 5, 0, 45, 8]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/oracle.json new file mode 100644 index 00000000..66e56aaa --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/oracle.json @@ -0,0 +1,34 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-labeled-control", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 2, + 3 + ], + "expectedThrow": null, + "semanticTags": [ + "labels", + "break-target", + "continue-target", + "nested-loop" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/source.js new file mode 100644 index 00000000..ea81c156 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-labeled-control/source.js @@ -0,0 +1,5 @@ +var breakCount = 0; +outer: for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { breakCount++; if (j === 1) break outer; } } +var continueCount = 0; +outer2: for (var x = 0; x < 3; x++) { for (var y = 0; y < 3; y++) { if (y === 1) continue outer2; continueCount++; } } +window.TEST_OUTPUT = [breakCount, continueCount]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/encoded.js new file mode 100644 index 00000000..0eea0bf5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */"bool", /* 4 */true, /* 5 */"number", /* 6 */42, /* 7 */"nan", /* 8 */"NaN", /* 9 */"infinity", /* 10 */"Infinity", /* 11 */"undef", /* 12 */"undefined", /* 13 */"array", /* 14 */"a", /* 15 */null, /* 16 */"object", /* 17 */"x", /* 18 */"y", /* 19 */"z", /* 20 */undefined, /* 21 */1]; +var BYTECODE = [4, 1, 47, 4, 188, 1, 7, 1, 0, 1, 2, 5, 3, 4, 0, 4, 0, 0, 5, 2, 4, 2, 4, 1, 0, 0, 5, 2, 0, 0, 6, 3, 0, 0, 7, 4, 0, 42, 8, 3, 1, 7, 0, 7, 5, 0, 0, 9, 6, 0, 42, 10, 3, 1, 9, 0, 9, 7, 0, 2, 11, 8, 0, 42, 12, 3, 1, 11, 0, 11, 9, 0, 2, 13, 10, 0, 42, 14, 3, 1, 13, 0, 13, 11, 0, 2, 15, 12, 0, 42, 16, 3, 1, 15, 0, 15, 13, 0, 0, 17, 14, 0, 42, 18, 3, 1, 17, 0, 17, 15, 0, 42, 19, 3, 1, 17, 48, 17, 2, 18, 19, 0, 18, 16, 0, 0, 19, 17, 0, 0, 20, 0, 0, 32, 21, 20, 42, 20, 3, 1, 21, 0, 21, 18, 0, 0, 22, 19, 0, 42, 23, 3, 1, 22, 49, 22, 2, 19, 20, 21, 23, 49, 19, 7, 6, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15, 17, 18, 22, 9, 4, 5, 19, 0, 4, 20, 0, 45, 4, 4, 2, 3, 3, 0, 5, 4, 3, 0, 5, 21, 0, 11, 6, 3, 5, 7, 0, 6, 48, 3, 2, 0, 4, 45, 3, 0, 3, 20, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 24; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/oracle.json new file mode 100644 index 00000000..22a54657 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/oracle.json @@ -0,0 +1,80 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-literals-order", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": { + "bool": [ + true, + 0 + ], + "number": [ + 42, + 1 + ], + "nan": [ + { + "type": "nan" + }, + 2 + ], + "infinity": [ + { + "type": "infinity" + }, + 3 + ], + "undef": [ + { + "type": "undefined" + }, + 4 + ], + "array": [ + [ + "a", + 5 + ], + [ + null, + 6 + ] + ], + "object": { + "x": [ + { + "type": "-0" + }, + 7 + ], + "y": [ + "z", + 8 + ] + } + }, + "expectedThrow": null, + "semanticTags": [ + "literals", + "nested-values", + "special-numbers", + "evaluation-order" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/source.js new file mode 100644 index 00000000..6bfe0b54 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/source.js @@ -0,0 +1,8 @@ +var index = 0; +function mark(value) { return [value, index++]; } +window.TEST_OUTPUT = { + bool: mark(true), number: mark(42), nan: mark(NaN), + infinity: mark(Infinity), undef: mark(undefined), + array: [mark("a"), mark(null)], + object: { x: mark(-0), y: mark("z") }, +}; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/encoded.js new file mode 100644 index 00000000..742ec7f8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */6, /* 2 */2, /* 3 */5, /* 4 */"push", /* 5 */1, /* 6 */3, /* 7 */"window", /* 8 */"TEST_OUTPUT", /* 9 */undefined]; +var BYTECODE = [4, 1, 48, 7, 0, 5, 2, 7, 0, 7, 0, 0, 5, 3, 7, 0, 7, 1, 0, 22, 8, 3, 7, 40, 8, 82, 0, 7, 2, 0, 26, 8, 3, 7, 40, 8, 39, 39, 66, 0, 7, 3, 0, 26, 8, 3, 7, 40, 8, 52, 39, 82, 0, 7, 4, 0, 8, 8, 2, 7, 43, 7, 2, 8, 1, 3, 5, 7, 3, 0, 7, 5, 0, 11, 8, 3, 7, 5, 3, 8, 39, 15, 0, 7, 0, 0, 5, 4, 7, 0, 7, 0, 0, 5, 5, 7, 0, 7, 6, 0, 22, 8, 5, 7, 40, 8, 184, 0, 7, 0, 0, 5, 6, 7, 0, 7, 6, 0, 22, 8, 6, 7, 40, 8, 168, 5, 7, 4, 0, 7, 5, 0, 11, 8, 4, 7, 5, 4, 8, 0, 7, 5, 0, 26, 8, 6, 7, 40, 8, 152, 39, 168, 5, 7, 6, 0, 7, 5, 0, 11, 8, 6, 7, 5, 6, 8, 39, 114, 5, 7, 5, 0, 7, 5, 0, 11, 8, 5, 7, 5, 5, 8, 39, 96, 2, 7, 7, 0, 0, 8, 8, 0, 48, 9, 2, 2, 4, 9, 7, 8, 9, 0, 7, 9, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/oracle.json new file mode 100644 index 00000000..84b7c47a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/oracle.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-loop-abrupt", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + [ + 0, + 1, + 3, + 4 + ], + 6 + ], + "expectedThrow": null, + "semanticTags": [ + "break", + "continue", + "nested-loop", + "abrupt-completion" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/source.js new file mode 100644 index 00000000..21c008e6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-abrupt/source.js @@ -0,0 +1,5 @@ +var values = []; +for (var i = 0; i < 6; i++) { if (i === 2) continue; if (i === 5) break; values.push(i); } +var nested = 0; +for (var r = 0; r < 3; r++) { for (var c = 0; c < 3; c++) { nested++; if (c === 1) break; } } +window.TEST_OUTPUT = [values, nested]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/encoded.js new file mode 100644 index 00000000..1268d01e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */1, /* 2 */5, /* 3 */2, /* 4 */3, /* 5 */"window", /* 6 */"TEST_OUTPUT", /* 7 */undefined]; +var BYTECODE = [4, 1, 0, 7, 0, 0, 5, 2, 7, 0, 7, 1, 0, 5, 3, 7, 0, 7, 2, 0, 24, 8, 3, 7, 40, 8, 50, 11, 7, 2, 3, 5, 2, 7, 5, 7, 3, 0, 7, 1, 0, 11, 8, 3, 7, 5, 3, 8, 39, 16, 0, 7, 0, 0, 5, 4, 7, 0, 7, 0, 0, 5, 5, 7, 5, 7, 5, 0, 7, 1, 0, 11, 8, 5, 7, 5, 5, 8, 5, 7, 4, 0, 7, 1, 0, 11, 8, 4, 7, 5, 4, 8, 0, 7, 3, 0, 22, 8, 4, 7, 40, 8, 105, 39, 64, 0, 7, 0, 0, 5, 6, 7, 0, 7, 4, 0, 22, 8, 6, 7, 40, 8, 139, 5, 7, 6, 0, 7, 1, 0, 11, 8, 6, 7, 5, 6, 8, 39, 112, 2, 7, 5, 0, 0, 8, 6, 0, 48, 9, 3, 2, 5, 6, 9, 7, 8, 9, 0, 7, 7, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/oracle.json new file mode 100644 index 00000000..af9aa41d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/oracle.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-loop-forms", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 15, + 2, + 3 + ], + "expectedThrow": null, + "semanticTags": [ + "for", + "while", + "do-while", + "loop-boundary" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/source.js new file mode 100644 index 00000000..079a4ba0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-loop-forms/source.js @@ -0,0 +1,4 @@ +var sum = 0; for (var i = 1; i <= 5; i++) sum += i; +var j = 0; var doCount = 0; do { doCount++; j++; } while (j < 2); +var w = 0; while (w < 3) w++; +window.TEST_OUTPUT = [sum, doCount, w]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/encoded.js new file mode 100644 index 00000000..1e442e69 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"RegExp", /* 1 */"a", /* 2 */"g", /* 3 */"test", /* 4 */"lastIndex", /* 5 */"x", /* 6 */"window", /* 7 */"TEST_OUTPUT", /* 8 */undefined]; +var BYTECODE = [4, 1, 2, 8, 0, 0, 0, 9, 1, 0, 0, 10, 2, 0, 44, 11, 8, 2, 9, 10, 5, 2, 11, 0, 8, 3, 0, 8, 9, 2, 8, 0, 8, 1, 0, 43, 10, 2, 9, 1, 8, 5, 3, 10, 0, 8, 4, 0, 8, 9, 2, 8, 5, 4, 9, 0, 8, 3, 0, 8, 9, 2, 8, 0, 8, 1, 0, 43, 10, 2, 9, 1, 8, 5, 5, 10, 0, 8, 4, 0, 8, 9, 2, 8, 5, 6, 9, 2, 8, 0, 0, 0, 9, 5, 0, 0, 10, 2, 0, 44, 11, 8, 2, 9, 10, 0, 8, 3, 0, 8, 9, 11, 8, 0, 8, 5, 0, 43, 10, 11, 9, 1, 8, 2, 8, 0, 0, 0, 9, 5, 0, 0, 11, 2, 0, 44, 12, 8, 2, 9, 11, 0, 8, 3, 0, 8, 9, 12, 8, 0, 8, 5, 0, 43, 11, 12, 9, 1, 8, 48, 8, 2, 10, 11, 5, 7, 8, 2, 8, 6, 0, 0, 9, 7, 0, 48, 10, 5, 3, 4, 5, 6, 7, 9, 8, 9, 10, 0, 8, 8, 0, 45, 8]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 13; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/oracle.json new file mode 100644 index 00000000..88a8ddea --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/oracle.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-regexp-state", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + true, + 1, + false, + 0, + [ + true, + true + ] + ], + "expectedThrow": null, + "semanticTags": [ + "regexp", + "state", + "lastIndex", + "fresh-literal" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/source.js new file mode 100644 index 00000000..f0caa611 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-regexp-state/source.js @@ -0,0 +1,7 @@ +var re = /a/g; +var first = re.test("a"); +var afterFirst = re.lastIndex; +var second = re.test("a"); +var afterSecond = re.lastIndex; +var fresh = [/x/g.test("x"), /x/g.test("x")]; +window.TEST_OUTPUT = [first, afterFirst, second, afterSecond, fresh]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/encoded.js new file mode 100644 index 00000000..046044ab --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */false, /* 1 */"and", /* 2 */true, /* 3 */"or", /* 4 */null, /* 5 */"null", /* 6 */0, /* 7 */"zero", /* 8 */"window", /* 9 */"TEST_OUTPUT", /* 10 */"values", /* 11 */"log", /* 12 */undefined, /* 13 */"push"]; +var BYTECODE = [4, 1, 47, 8, 176, 1, 6, 1, 0, 1, 2, 5, 3, 8, 48, 8, 0, 5, 2, 8, 0, 8, 0, 0, 5, 9, 8, 40, 9, 42, 0, 8, 1, 0, 42, 10, 3, 1, 8, 5, 9, 10, 5, 4, 9, 0, 8, 2, 0, 5, 9, 8, 41, 9, 67, 0, 8, 3, 0, 42, 10, 3, 1, 8, 5, 9, 10, 5, 5, 9, 0, 8, 4, 0, 5, 9, 8, 0, 8, 4, 0, 28, 10, 9, 8, 40, 10, 100, 0, 8, 5, 0, 42, 10, 3, 1, 8, 5, 9, 10, 5, 6, 9, 0, 8, 6, 0, 5, 9, 8, 0, 8, 4, 0, 28, 10, 9, 8, 40, 10, 133, 0, 8, 7, 0, 42, 10, 3, 1, 8, 5, 9, 10, 5, 7, 9, 2, 8, 8, 0, 0, 9, 9, 0, 0, 10, 10, 0, 48, 11, 4, 4, 5, 6, 7, 0, 12, 11, 0, 49, 13, 2, 10, 11, 12, 2, 9, 8, 9, 13, 0, 8, 12, 0, 45, 8, 4, 2, 3, 3, 0, 0, 4, 13, 0, 8, 5, 3, 4, 43, 4, 3, 5, 1, 0, 45, 0, 0, 3, 12, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 14; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/oracle.json new file mode 100644 index 00000000..eb303182 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/oracle.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-short-circuit", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": { + "values": [ + false, + true, + "null", + 0 + ], + "log": [ + "null" + ] + }, + "expectedThrow": null, + "semanticTags": [ + "short-circuit", + "nullish", + "evaluation-order", + "side-effects" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/source.js new file mode 100644 index 00000000..e9c3774c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-short-circuit/source.js @@ -0,0 +1,7 @@ +var log = []; +function hit(value) { log.push(value); return value; } +var andValue = false && hit("and"); +var orValue = true || hit("or"); +var nullValue = null ?? hit("null"); +var zeroValue = 0 ?? hit("zero"); +window.TEST_OUTPUT = { values: [andValue, orValue, nullValue, zeroValue], log: log }; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/encoded.js new file mode 100644 index 00000000..1ea3ca16 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */1, /* 1 */2, /* 2 */"a", /* 3 */"b", /* 4 */"Object", /* 5 */"assign", /* 6 */3, /* 7 */"window", /* 8 */"TEST_OUTPUT", /* 9 */"concat", /* 10 */undefined]; +var BYTECODE = [4, 1, 47, 5, 157, 3, 7, 0, 0, 5, 2, 5, 0, 5, 0, 0, 0, 6, 1, 0, 48, 7, 2, 5, 6, 5, 3, 7, 49, 5, 0, 0, 6, 2, 0, 0, 7, 0, 0, 9, 5, 6, 7, 0, 6, 3, 0, 0, 7, 1, 0, 9, 5, 6, 7, 2, 6, 4, 0, 0, 7, 5, 0, 8, 8, 6, 7, 0, 7, 2, 0, 0, 9, 6, 0, 49, 10, 1, 7, 9, 43, 7, 6, 8, 2, 5, 10, 5, 4, 5, 2, 5, 7, 0, 0, 6, 8, 0, 48, 7, 0, 0, 8, 9, 0, 8, 9, 7, 8, 43, 8, 7, 9, 1, 3, 0, 7, 9, 0, 8, 9, 8, 7, 0, 7, 6, 0, 48, 10, 1, 7, 43, 7, 8, 9, 1, 10, 42, 8, 2, 65535, 7, 48, 7, 2, 8, 4, 9, 5, 6, 7, 0, 5, 10, 0, 45, 5, 4, 4, 11, 5, 0, 1, 11, 6, 5, 2, 45, 6, 0, 5, 10, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/oracle.json new file mode 100644 index 00000000..39efba9e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/oracle.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-spread-order", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 6, + { + "a": 3, + "b": 2 + } + ], + "expectedThrow": null, + "semanticTags": [ + "spread", + "call-arguments", + "object-spread", + "overwrite-order" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/source.js new file mode 100644 index 00000000..1f39dcd2 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/source.js @@ -0,0 +1,4 @@ +function sum(a, b, c) { return a + b + c; } +var values = [1, 2]; +var merged = { a: 1, b: 2, ...{ a: 3 } }; +window.TEST_OUTPUT = [sum(...values, 3), merged]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/encoded.js new file mode 100644 index 00000000..0e1564d2 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */2, /* 4 */3, /* 5 */4, /* 6 */5, /* 7 */undefined, /* 8 */"push", /* 9 */"one", /* 10 */"two", /* 11 */"three", /* 12 */"default", /* 13 */"low", /* 14 */"other", /* 15 */0, /* 16 */"a", /* 17 */"b", /* 18 */"d"]; +var BYTECODE = [4, 1, 47, 5, 143, 1, 7, 0, 0, 5, 2, 5, 47, 5, 276, 1, 7, 0, 0, 5, 3, 5, 47, 5, 389, 1, 8, 0, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 2, 1, 7, 0, 7, 3, 0, 42, 9, 2, 1, 7, 0, 7, 4, 0, 42, 10, 2, 1, 7, 0, 7, 5, 0, 42, 11, 2, 1, 7, 0, 7, 2, 0, 42, 12, 3, 1, 7, 0, 7, 3, 0, 42, 13, 3, 1, 7, 0, 7, 4, 0, 42, 14, 3, 1, 7, 0, 7, 5, 0, 42, 15, 3, 1, 7, 0, 7, 6, 0, 42, 16, 4, 1, 7, 48, 7, 9, 8, 9, 10, 11, 12, 13, 14, 15, 16, 9, 5, 6, 7, 0, 5, 7, 0, 45, 5, 4, 2, 48, 4, 0, 5, 3, 4, 0, 4, 2, 0, 26, 5, 0, 4, 40, 5, 164, 39, 192, 0, 4, 3, 0, 26, 5, 0, 4, 40, 5, 177, 39, 212, 0, 4, 4, 0, 26, 5, 0, 4, 40, 5, 190, 39, 230, 39, 250, 0, 4, 8, 0, 8, 5, 3, 4, 0, 4, 9, 0, 43, 6, 3, 5, 1, 4, 39, 268, 0, 4, 8, 0, 8, 5, 3, 4, 0, 4, 10, 0, 43, 6, 3, 5, 1, 4, 0, 4, 8, 0, 8, 5, 3, 4, 0, 4, 11, 0, 43, 6, 3, 5, 1, 4, 39, 268, 0, 4, 8, 0, 8, 5, 3, 4, 0, 4, 12, 0, 43, 6, 3, 5, 1, 4, 45, 3, 0, 4, 7, 0, 45, 4, 4, 2, 48, 4, 0, 5, 3, 4, 0, 4, 2, 0, 26, 5, 0, 4, 40, 5, 297, 39, 325, 0, 4, 3, 0, 26, 5, 0, 4, 40, 5, 310, 39, 325, 0, 4, 4, 0, 26, 5, 0, 4, 40, 5, 323, 39, 363, 39, 345, 0, 4, 8, 0, 8, 5, 3, 4, 0, 4, 13, 0, 43, 6, 3, 5, 1, 4, 39, 381, 0, 4, 8, 0, 8, 5, 3, 4, 0, 4, 14, 0, 43, 6, 3, 5, 1, 4, 0, 4, 8, 0, 8, 5, 3, 4, 0, 4, 11, 0, 43, 6, 3, 5, 1, 4, 45, 3, 0, 4, 7, 0, 45, 4, 4, 2, 48, 5, 0, 5, 3, 5, 0, 5, 15, 0, 5, 4, 5, 22, 5, 4, 0, 40, 5, 540, 0, 5, 15, 0, 26, 6, 4, 5, 40, 6, 424, 39, 452, 0, 5, 2, 0, 26, 6, 4, 5, 40, 6, 437, 39, 470, 0, 5, 3, 0, 26, 6, 4, 5, 40, 6, 450, 39, 490, 39, 492, 0, 5, 8, 0, 8, 6, 3, 5, 0, 5, 16, 0, 43, 7, 3, 6, 1, 5, 0, 5, 8, 0, 8, 6, 3, 5, 0, 5, 17, 0, 43, 7, 3, 6, 1, 5, 39, 510, 39, 524, 0, 5, 8, 0, 8, 6, 3, 5, 0, 5, 18, 0, 43, 7, 3, 6, 1, 5, 0, 5, 8, 0, 8, 6, 3, 5, 43, 5, 3, 6, 1, 4, 5, 5, 4, 0, 5, 2, 0, 11, 6, 4, 5, 5, 4, 6, 39, 404, 45, 3, 0, 5, 7, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 17; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/generation.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/generation.json new file mode 100644 index 00000000..03b320ae --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/generation.json @@ -0,0 +1,11 @@ +{ + "note": "encoded.js is the VM encoder's output for source.js with default options, which draw no randomness, with the leading disassembly comment block removed (the committed corpus format). Regenerating with the same encoder revision reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5", + "options": {} + }, + "cells": { + "f-switch-flow": "one function per switch shape whose case bodies share routes: pick, a case falling into the next between breaking cases and a default; group, grouped empty cases and a default in the middle falling into the last case; walk, cases falling through inside a for loop with a case that continues; each run with inputs that take every route" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/oracle.json new file mode 100644 index 00000000..3e27abb9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/oracle.json @@ -0,0 +1,71 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-switch-flow", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + [ + "one" + ], + [ + "two", + "three" + ], + [ + "three" + ], + [ + "default" + ], + [ + "low" + ], + [ + "low" + ], + [ + "three" + ], + [ + "other", + "three" + ], + [ + "a", + "b", + 0, + "b", + 1, + "d", + 3, + "d", + 4 + ] + ], + "expectedThrow": null, + "semanticTags": [ + "switch", + "fall-through", + "case-selection", + "grouped-cases", + "default-position", + "switch-in-loop" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/source.js new file mode 100644 index 00000000..928ac0d7 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-switch-flow/source.js @@ -0,0 +1,4 @@ +function pick(x) { var out = []; switch (x) { case 1: out.push("one"); break; case 2: out.push("two"); case 3: out.push("three"); break; default: out.push("default"); } return out; } +function group(x) { var out = []; switch (x) { case 1: case 2: out.push("low"); break; default: out.push("other"); case 3: out.push("three"); } return out; } +function walk(n) { var out = []; for (var i = 0; i < n; i++) { switch (i) { case 0: out.push("a"); case 1: out.push("b"); break; case 2: continue; default: out.push("d"); } out.push(i); } return out; } +window.TEST_OUTPUT = [pick(1), pick(2), pick(3), pick(4), group(1), group(2), group(3), group(4), walk(5)]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/encoded.js new file mode 100644 index 00000000..f093161a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"Hello ", /* 3 */"World", /* 4 */"!", /* 5 */"", /* 6 */1, /* 7 */2, /* 8 */"just a string", /* 9 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 11, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 0, 6, 6, 0, 0, 7, 7, 0, 11, 8, 6, 7, 11, 6, 4, 8, 0, 4, 5, 0, 11, 7, 6, 4, 0, 4, 8, 0, 48, 6, 3, 5, 7, 4, 9, 2, 3, 6, 0, 2, 9, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/oracle.json new file mode 100644 index 00000000..fc94389b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/oracle.json @@ -0,0 +1,34 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-template-literals", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + "Hello World!", + "3", + "just a string" + ], + "expectedThrow": null, + "semanticTags": [ + "template-literal", + "interpolation", + "es6-limited" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/source.js new file mode 100644 index 00000000..a37ea344 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-template-literals/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = [`Hello ${"World"}!`, `${1 + 2}`, `just a string`]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/encoded.js new file mode 100644 index 00000000..6467c9e9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"call", /* 1 */"base", /* 2 */1, /* 3 */5, /* 4 */"apply", /* 5 */2, /* 6 */7, /* 7 */"bind", /* 8 */3, /* 9 */"window", /* 10 */"TEST_OUTPUT", /* 11 */4, /* 12 */undefined]; +var BYTECODE = [4, 1, 47, 6, 149, 1, 5, 0, 0, 5, 2, 6, 0, 6, 0, 0, 8, 7, 2, 6, 0, 6, 1, 0, 0, 8, 2, 0, 49, 9, 1, 6, 8, 0, 6, 3, 0, 43, 8, 2, 7, 2, 9, 6, 5, 3, 8, 0, 6, 4, 0, 8, 7, 2, 6, 0, 6, 1, 0, 0, 8, 5, 0, 49, 9, 1, 6, 8, 0, 6, 6, 0, 48, 8, 1, 6, 43, 6, 2, 7, 2, 9, 8, 5, 4, 6, 0, 6, 7, 0, 8, 7, 2, 6, 0, 6, 1, 0, 0, 8, 8, 0, 49, 9, 1, 6, 8, 43, 6, 2, 7, 1, 9, 5, 5, 6, 2, 6, 9, 0, 0, 7, 10, 0, 0, 8, 11, 0, 42, 9, 5, 1, 8, 48, 8, 3, 3, 4, 9, 9, 6, 7, 8, 0, 6, 12, 0, 45, 6, 4, 2, 0, 3, 1, 0, 8, 4, 2, 3, 11, 3, 4, 0, 45, 3, 0, 3, 12, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/oracle.json new file mode 100644 index 00000000..27227a47 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/oracle.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-this-explicit", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 6, + 9, + 7 + ], + "expectedThrow": null, + "semanticTags": [ + "call", + "apply", + "bind", + "receiver" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/source.js new file mode 100644 index 00000000..ae6ccb79 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-explicit/source.js @@ -0,0 +1,5 @@ +function add(value) { return this.base + value; } +var first = add.call({ base: 1 }, 5); +var second = add.apply({ base: 2 }, [7]); +var bound = add.bind({ base: 3 }); +window.TEST_OUTPUT = [first, second, bound(4)]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/encoded.js new file mode 100644 index 00000000..f722ffdc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"prototype", /* 1 */"get", /* 2 */7, /* 3 */"value", /* 4 */3, /* 5 */"window", /* 6 */"TEST_OUTPUT", /* 7 */"call", /* 8 */9, /* 9 */undefined]; +var BYTECODE = [4, 1, 47, 5, 161, 1, 4, 0, 0, 5, 2, 5, 0, 5, 0, 0, 8, 6, 2, 5, 0, 5, 1, 0, 47, 7, 177, 0, 4, 0, 0, 9, 6, 5, 7, 0, 5, 2, 0, 44, 6, 2, 1, 5, 5, 3, 6, 0, 5, 3, 0, 0, 6, 4, 0, 0, 7, 1, 0, 47, 8, 195, 0, 4, 0, 0, 49, 9, 2, 5, 6, 7, 8, 5, 4, 9, 2, 5, 5, 0, 0, 6, 6, 0, 0, 7, 1, 0, 8, 8, 3, 7, 43, 7, 3, 8, 0, 0, 8, 1, 0, 8, 9, 4, 8, 43, 8, 4, 9, 0, 0, 9, 1, 0, 8, 10, 4, 9, 0, 9, 7, 0, 8, 11, 10, 9, 0, 9, 3, 0, 0, 12, 8, 0, 49, 13, 1, 9, 12, 43, 9, 10, 11, 1, 13, 48, 10, 3, 7, 8, 9, 9, 5, 6, 10, 0, 5, 9, 0, 45, 5, 4, 2, 0, 3, 3, 0, 9, 2, 3, 0, 0, 3, 9, 0, 45, 3, 4, 1, 0, 2, 3, 0, 8, 3, 1, 2, 45, 3, 0, 2, 9, 0, 45, 2, 4, 1, 0, 2, 3, 0, 8, 3, 1, 2, 45, 3, 0, 2, 9, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 14; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/oracle.json new file mode 100644 index 00000000..6d48b32b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/oracle.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-this-receiver", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": [ + 7, + 3, + 9 + ], + "expectedThrow": null, + "semanticTags": [ + "this", + "new", + "prototype", + "method-call" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/source.js new file mode 100644 index 00000000..e979c647 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-this-receiver/source.js @@ -0,0 +1,5 @@ +function Box(value) { this.value = value; } +Box.prototype.get = function () { return this.value; }; +var box = new Box(7); +var object = { value: 3, get: function () { return this.value; } }; +window.TEST_OUTPUT = [box.get(), object.get(), object.get.call({ value: 9 })]; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/encoded.js new file mode 100644 index 00000000..733eb79f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"boom", /* 1 */"push", /* 2 */"type", /* 3 */"value", /* 4 */"Error", /* 5 */"bad", /* 6 */"name", /* 7 */"message", /* 8 */"kind", /* 9 */"uncaught", /* 10 */"caught", /* 11 */undefined]; +var BYTECODE = [4, 1, 48, 4, 0, 5, 2, 4, 54, 20, 3, 0, 4, 0, 0, 46, 4, 55, 39, 52, 0, 4, 1, 0, 8, 5, 2, 4, 0, 4, 2, 0, 36, 6, 3, 0, 7, 3, 0, 49, 8, 2, 4, 6, 7, 3, 43, 4, 2, 5, 1, 8, 54, 73, 3, 2, 4, 4, 0, 0, 5, 5, 0, 44, 6, 4, 1, 5, 46, 6, 55, 39, 118, 0, 4, 1, 0, 8, 5, 2, 4, 0, 4, 2, 0, 0, 6, 6, 0, 8, 7, 3, 6, 0, 6, 7, 0, 0, 8, 7, 0, 8, 9, 3, 8, 49, 8, 2, 4, 7, 6, 9, 43, 4, 2, 5, 1, 8, 0, 4, 8, 0, 0, 5, 9, 0, 0, 6, 10, 0, 49, 7, 2, 4, 5, 6, 2, 46, 7, 0, 4, 11, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/oracle.json new file mode 100644 index 00000000..96939465 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/oracle.json @@ -0,0 +1,47 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "f-throw-catch", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [], + "timeoutMs": 2000, + "expected": null, + "expectedThrow": { + "type": "primitive", + "value": { + "kind": "uncaught", + "caught": [ + { + "type": "string", + "value": "boom" + }, + { + "type": "Error", + "message": "bad" + } + ] + } + }, + "semanticTags": [ + "throw", + "catch", + "uncaught", + "primitive-error", + "error-object" + ], + "hazards": [ + "The cell intentionally has no successful TEST_OUTPUT; source/raw/stripped must agree on the exact tagged uncaught object and its caught evidence.", + "Error stack text is excluded; only the stable Error name/message and primitive caught value are retained." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "tagged-throw" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/source.js new file mode 100644 index 00000000..58ffab3c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/f-throw-catch/source.js @@ -0,0 +1,4 @@ +var result = []; +try { throw "boom"; } catch (error) { result.push({ type: typeof error, value: error }); } +try { throw new Error("bad"); } catch (error) { result.push({ type: error.name, message: error.message }); } +throw { kind: "uncaught", caught: result }; diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/encoded.js new file mode 100644 index 00000000..9c27b19b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */undefined, /* 1 */"document", /* 2 */"window", /* 3 */"documentElement", /* 4 */"createElement", /* 5 */"bind", /* 6 */"div", /* 7 */"table", /* 8 */"tbody", /* 9 */"tr", /* 10 */"Array", /* 11 */"isArray", /* 12 */"prototype", /* 13 */"concat", /* 14 */"filter", /* 15 */"indexOf", /* 16 */"map", /* 17 */"push", /* 18 */"slice", /* 19 */"some", /* 20 */"splice", /* 21 */"RegExp", /* 22 */"^#(?:[\\w-]|\\\\.|[^\\x00-\\xa0])*$", /* 23 */"", /* 24 */"^\\.(?:[\\w-]|\\\\.|[^\\x00-\\xa0])*$", /* 25 */"<.+>", /* 26 */"^\\w+$", /* 27 */"init", /* 28 */"fn", /* 29 */"length", /* 30 */0, /* 31 */"Symbol", /* 32 */"function", /* 33 */"iterator", /* 34 */"isWindow", /* 35 */"isFunction", /* 36 */"isNumeric", /* 37 */"isPlainObject", /* 38 */"each", /* 39 */"empty", /* 40 */"extend", /* 41 */"\\S+", /* 42 */"g", /* 43 */"toggleClass", /* 44 */"addClass", /* 45 */"removeAttr", /* 46 */"attr", /* 47 */"removeClass", /* 48 */"hasClass", /* 49 */"get", /* 50 */"eq", /* 51 */"first", /* 52 */"last", /* 53 */"text", /* 54 */"detach", /* 55 */"^\\s*<(\\w+)[^>]*>", /* 56 */"^<(\\w+)\\s*\\/?>(?:<\\/\\1>)?$", /* 57 */"*", /* 58 */"td", /* 59 */"th", /* 60 */"thead", /* 61 */"tfoot", /* 62 */"parseHTML", /* 63 */"has", /* 64 */"not", /* 65 */"val", /* 66 */"is", /* 67 */"guid", /* 68 */1, /* 69 */"unique", /* 70 */"add", /* 71 */"children", /* 72 */"parent", /* 73 */"index", /* 74 */"closest", /* 75 */"siblings", /* 76 */"find", /* 77 */"^\\s*\\s*$", /* 78 */"^$|^module$|\\/(java|ecma)script", /* 79 */"i", /* 80 */"type", /* 81 */"src", /* 82 */"nonce", /* 83 */"noModule", /* 84 */"after", /* 85 */"append", /* 86 */"html", /* 87 */"appendTo", /* 88 */"wrapInner", /* 89 */"before", /* 90 */"wrapAll", /* 91 */"wrap", /* 92 */"insertAfter", /* 93 */"insertBefore", /* 94 */"prepend", /* 95 */"prependTo", /* 96 */"contents", /* 97 */"next", /* 98 */"nextAll", /* 99 */"nextUntil", /* 100 */"parents", /* 101 */"parentsUntil", /* 102 */"prev", /* 103 */"prevAll", /* 104 */"prevUntil", /* 105 */"clone", /* 106 */"offsetParent", /* 107 */"-([a-z])", /* 108 */"ready", /* 109 */"unwrap", /* 110 */"offset", /* 111 */"position", /* 112 */"class", /* 113 */"className", /* 114 */"contenteditable", /* 115 */"contentEditable", /* 116 */"for", /* 117 */"htmlFor", /* 118 */"readonly", /* 119 */"readOnly", /* 120 */"maxlength", /* 121 */"maxLength", /* 122 */"tabindex", /* 123 */"tabIndex", /* 124 */"colspan", /* 125 */"colSpan", /* 126 */"rowspan", /* 127 */"rowSpan", /* 128 */"usemap", /* 129 */"useMap", /* 130 */"prop", /* 131 */"removeProp", /* 132 */"^--", /* 133 */"style", /* 134 */"webkit", /* 135 */"moz", /* 136 */"ms", /* 137 */"animationIterationCount", /* 138 */"columnCount", /* 139 */"flexGrow", /* 140 */"flexShrink", /* 141 */"fontWeight", /* 142 */"gridArea", /* 143 */"gridColumn", /* 144 */"gridColumnEnd", /* 145 */"gridColumnStart", /* 146 */"gridRow", /* 147 */"gridRowEnd", /* 148 */"gridRowStart", /* 149 */"lineHeight", /* 150 */"opacity", /* 151 */"order", /* 152 */"orphans", /* 153 */"widows", /* 154 */"zIndex", /* 155 */"css", /* 156 */"^\\s+|\\s+$", /* 157 */"data", /* 158 */"Width", /* 159 */"Height", /* 160 */"___cd", /* 161 */"toggle", /* 162 */"hide", /* 163 */"show", /* 164 */"___ce", /* 165 */".", /* 166 */"focus", /* 167 */"focusin", /* 168 */"blur", /* 169 */"focusout", /* 170 */"mouseenter", /* 171 */"mouseover", /* 172 */"mouseleave", /* 173 */"mouseout", /* 174 */"^(mouse|pointer|contextmenu|drag|drop|click|dblclick)", /* 175 */"trigger", /* 176 */"off", /* 177 */"remove", /* 178 */"replaceWith", /* 179 */"replaceAll", /* 180 */"on", /* 181 */"one", /* 182 */"\\r?\\n", /* 183 */"file|reset|submit|button|image", /* 184 */"radio|checkbox", /* 185 */"serialize", /* 186 */"exports", /* 187 */"u", /* 188 */"module", /* 189 */"cash", /* 190 */"$", /* 191 */"test", /* 192 */"getElementsByClassName", /* 193 */"replace", /* 194 */"\\\\", /* 195 */"getElementsByTagName", /* 196 */"querySelectorAll", /* 197 */"nodeType", /* 198 */9, /* 199 */11, /* 200 */3, /* 201 */"boolean", /* 202 */"string", /* 203 */null, /* 204 */"isNaN", /* 205 */"parseFloat", /* 206 */"isFinite", /* 207 */"object", /* 208 */"Object", /* 209 */"getPrototypeOf", /* 210 */"call", /* 211 */"keys", /* 212 */"shift", /* 213 */"constructor", /* 214 */"match", /* 215 */2, /* 216 */"getAttribute", /* 217 */"setAttribute", /* 218 */"join", /* 219 */"textContent", /* 220 */"getComputedStyle", /* 221 */"getPropertyValue", /* 222 */"parseInt", /* 223 */10, /* 224 */"border", /* 225 */"Left", /* 226 */"Top", /* 227 */"padding", /* 228 */"Right", /* 229 */"Bottom", /* 230 */"body", /* 231 */"display", /* 232 */"removeChild", /* 233 */"none", /* 234 */"block", /* 235 */"matches", /* 236 */"webkitMatchesSelector", /* 237 */"msMatchesSelector", /* 238 */"$1", /* 239 */"innerHTML", /* 240 */"childNodes", /* 241 */"apply", /* 242 */"multiple", /* 243 */"options", /* 244 */"value", /* 245 */"selected", /* 246 */"disabled", /* 247 */"parentNode", /* 248 */"String", /* 249 */"checked", /* 250 */"script", /* 251 */"contains", /* 252 */"head", /* 253 */"firstChild", /* 254 */"nodeName", /* 255 */"HTML", /* 256 */"replaceChild", /* 257 */"nextSibling", /* 258 */"ownerDocument", /* 259 */"cloneNode", /* 260 */"]", /* 261 */"toUpperCase", /* 262 */" ", /* 263 */"split", /* 264 */"px", /* 265 */"setProperty", /* 266 */"dataset", /* 267 */"JSON", /* 268 */"parse", /* 269 */"stringify", /* 270 */"Math", /* 271 */"max", /* 272 */"scroll", /* 273 */"client", /* 274 */"sort", /* 275 */"addEventListener", /* 276 */"removeEventListener", /* 277 */"target", /* 278 */"___i", /* 279 */"stopImmediatePropagation", /* 280 */"namespace", /* 281 */"___ot", /* 282 */"relatedTarget", /* 283 */"defineProperty", /* 284 */"currentTarget", /* 285 */"configurable", /* 286 */"delegateTarget", /* 287 */"___td", /* 288 */"preventDefault", /* 289 */"stopPropagation", /* 290 */"&", /* 291 */"encodeURIComponent", /* 292 */"=", /* 293 */"\r\n", /* 294 */"getElementById", /* 295 */"classList", /* 296 */"removeAttribute", /* 297 */"Number", /* 298 */"firstElementChild", /* 299 */"tagName", /* 300 */"IFRAME", /* 301 */"contentDocument", /* 302 */"TEMPLATE", /* 303 */"content", /* 304 */"nextElementSibling", /* 305 */"parentElement", /* 306 */"previousElementSibling", /* 307 */"static", /* 308 */"readyState", /* 309 */"loading", /* 310 */"DOMContentLoaded", /* 311 */"setTimeout", /* 312 */"BODY", /* 313 */"getBoundingClientRect", /* 314 */"top", /* 315 */"pageYOffset", /* 316 */"left", /* 317 */"pageXOffset", /* 318 */"fixed", /* 319 */"borderTopWidth", /* 320 */"borderLeftWidth", /* 321 */"marginTop", /* 322 */"marginLeft", /* 323 */"outer", /* 324 */"inner", /* 325 */"margin", /* 326 */"toLowerCase", /* 327 */"boxSizing", /* 328 */"border-box", /* 329 */"MouseEvents", /* 330 */"HTMLEvents", /* 331 */"createEvent", /* 332 */"initEvent", /* 333 */"dispatchEvent", /* 334 */"elements", /* 335 */"name", /* 336 */"FIELDSET"]; +var BYTECODE = [4, 1, 47, 2, 19, 0, 144, 0, 0, 42, 3, 2, 0, 0, 2, 0, 0, 45, 2, 4, 1, 47, 107, 3529, 2, 13, 5, 0, 1, 31, 1, 30, 1, 32, 1, 21, 1, 23, 5, 24, 107, 47, 107, 3796, 1, 5, 1, 0, 1, 25, 5, 28, 107, 47, 107, 3813, 1, 6, 0, 0, 5, 29, 107, 47, 107, 3850, 1, 7, 0, 0, 5, 30, 107, 47, 107, 3891, 1, 7, 0, 0, 5, 31, 107, 47, 107, 3932, 1, 7, 0, 0, 5, 32, 107, 47, 107, 3973, 1, 7, 0, 0, 5, 33, 107, 47, 107, 4014, 1, 6, 0, 0, 5, 34, 107, 47, 107, 4035, 1, 6, 0, 0, 5, 35, 107, 47, 107, 4056, 1, 6, 0, 0, 5, 36, 107, 47, 107, 4077, 1, 5, 0, 0, 5, 37, 107, 47, 107, 4098, 1, 5, 0, 0, 5, 38, 107, 47, 107, 4116, 1, 6, 0, 0, 5, 39, 107, 47, 107, 4165, 1, 8, 0, 0, 5, 40, 107, 47, 107, 4269, 3, 13, 1, 0, 1, 40, 5, 41, 107, 47, 107, 4557, 0, 16, 5, 0, 1, 34, 1, 42, 1, 27, 1, 10, 1, 40, 5, 42, 107, 47, 107, 4929, 1, 6, 2, 0, 1, 36, 1, 43, 5, 44, 107, 47, 107, 4990, 2, 12, 4, 0, 1, 36, 1, 32, 1, 38, 1, 37, 5, 45, 107, 47, 107, 5292, 1, 7, 3, 0, 1, 37, 1, 32, 1, 33, 5, 46, 107, 47, 107, 5496, 3, 10, 2, 0, 1, 32, 1, 3, 5, 47, 107, 47, 107, 5608, 2, 8, 1, 0, 1, 47, 5, 48, 107, 47, 107, 5654, 2, 11, 1, 0, 1, 48, 5, 49, 107, 47, 107, 5870, 1, 9, 4, 0, 1, 50, 1, 5, 1, 2, 1, 47, 5, 51, 107, 47, 107, 6011, 1, 6, 1, 0, 1, 47, 5, 52, 107, 47, 107, 6042, 2, 9, 0, 0, 5, 53, 107, 47, 107, 6151, 1, 8, 4, 0, 1, 36, 1, 53, 1, 35, 1, 28, 5, 54, 107, 47, 107, 6348, 2, 6, 0, 0, 5, 55, 107, 47, 107, 6383, 1, 9, 6, 0, 1, 36, 1, 57, 1, 5, 1, 56, 1, 58, 1, 27, 5, 59, 107, 47, 107, 6579, 4, 17, 3, 0, 1, 35, 1, 54, 1, 16, 5, 60, 107, 47, 107, 6813, 1, 9, 2, 0, 1, 60, 1, 13, 5, 61, 107, 47, 107, 6992, 1, 8, 7, 0, 1, 106, 1, 10, 1, 15, 1, 38, 1, 41, 1, 37, 1, 61, 5, 62, 107, 47, 107, 7444, 1, 7, 2, 0, 1, 13, 1, 14, 5, 63, 107, 47, 107, 7540, 2, 10, 7, 0, 1, 27, 1, 65, 1, 4, 1, 5, 1, 64, 1, 41, 1, 66, 5, 67, 107, 47, 107, 7875, 5, 14, 1, 0, 1, 67, 5, 68, 107, 47, 107, 8050, 8, 13, 3, 0, 1, 41, 1, 27, 1, 68, 5, 69, 107, 47, 107, 8319, 1, 8, 3, 0, 1, 37, 1, 32, 1, 27, 5, 70, 107, 47, 107, 8543, 1, 7, 1, 0, 1, 71, 5, 72, 107, 47, 107, 8601, 1, 6, 1, 0, 1, 74, 5, 75, 107, 47, 107, 8628, 2, 15, 6, 0, 1, 75, 1, 76, 1, 72, 1, 78, 1, 41, 1, 77, 5, 79, 107, 47, 107, 8950, 3, 9, 3, 0, 1, 75, 1, 80, 1, 39, 5, 81, 107, 47, 107, 9064, 2, 11, 6, 0, 1, 36, 1, 75, 1, 79, 1, 47, 1, 81, 1, 32, 5, 82, 107, 47, 107, 9342, 2, 6, 0, 0, 5, 83, 107, 47, 107, 9366, 2, 10, 3, 0, 1, 72, 1, 84, 1, 83, 5, 85, 107, 47, 107, 9469, 3, 9, 2, 0, 1, 83, 1, 72, 5, 86, 107, 47, 107, 9521, 2, 13, 4, 0, 1, 85, 1, 36, 1, 37, 1, 86, 5, 87, 107, 47, 107, 9789, 2, 14, 0, 0, 5, 88, 107, 47, 107, 9958, 1, 6, 2, 0, 1, 93, 1, 92, 5, 95, 107, 47, 107, 10000, 1, 8, 1, 0, 1, 91, 5, 96, 107, 47, 107, 10074, 1, 6, 1, 0, 1, 90, 5, 97, 107, 47, 107, 10110, 5, 12, 1, 0, 1, 97, 5, 98, 107, 47, 107, 10188, 2, 9, 1, 0, 1, 18, 5, 99, 107, 47, 107, 10275, 5, 14, 3, 0, 1, 97, 1, 99, 1, 100, 5, 100, 107, 47, 107, 10538, 5, 14, 19, 0, 1, 36, 1, 37, 1, 38, 1, 35, 1, 41, 1, 44, 1, 96, 1, 95, 1, 93, 1, 92, 1, 32, 1, 30, 1, 29, 1, 99, 1, 91, 1, 53, 1, 100, 1, 27, 1, 98, 5, 101, 107, 47, 107, 11804, 4, 9, 0, 0, 5, 102, 107, 47, 107, 11839, 2, 11, 1, 0, 1, 103, 5, 104, 107, 2, 107, 1, 0, 5, 2, 107, 2, 107, 2, 0, 5, 3, 107, 0, 107, 3, 0, 8, 108, 2, 107, 5, 4, 108, 0, 107, 4, 0, 8, 108, 2, 107, 0, 107, 5, 0, 8, 109, 108, 107, 43, 107, 108, 109, 1, 2, 5, 5, 107, 0, 107, 6, 0, 42, 108, 5, 1, 107, 5, 6, 108, 0, 107, 7, 0, 42, 108, 5, 1, 107, 5, 7, 108, 0, 107, 8, 0, 42, 108, 5, 1, 107, 5, 8, 108, 0, 107, 9, 0, 42, 108, 5, 1, 107, 5, 9, 108, 2, 107, 10, 0, 0, 108, 11, 0, 8, 109, 107, 108, 5, 10, 109, 2, 107, 10, 0, 0, 108, 12, 0, 8, 109, 107, 108, 5, 11, 109, 0, 107, 13, 0, 8, 108, 11, 107, 5, 12, 108, 0, 107, 14, 0, 8, 108, 11, 107, 5, 13, 108, 0, 107, 15, 0, 8, 108, 11, 107, 5, 14, 108, 0, 107, 16, 0, 8, 108, 11, 107, 5, 15, 108, 0, 107, 17, 0, 8, 108, 11, 107, 5, 16, 108, 0, 107, 18, 0, 8, 108, 11, 107, 5, 17, 108, 0, 107, 19, 0, 8, 108, 11, 107, 5, 18, 108, 0, 107, 20, 0, 8, 108, 11, 107, 5, 19, 108, 2, 107, 21, 0, 0, 108, 22, 0, 0, 109, 23, 0, 44, 110, 107, 2, 108, 109, 5, 20, 110, 2, 107, 21, 0, 0, 108, 24, 0, 0, 109, 23, 0, 44, 110, 107, 2, 108, 109, 5, 21, 110, 2, 107, 21, 0, 0, 108, 25, 0, 0, 109, 23, 0, 44, 110, 107, 2, 108, 109, 5, 22, 110, 2, 107, 21, 0, 0, 108, 26, 0, 0, 109, 23, 0, 44, 110, 107, 2, 108, 109, 5, 23, 110, 47, 107, 11926, 0, 6, 11, 0, 1, 28, 1, 36, 1, 2, 1, 20, 1, 30, 1, 22, 1, 59, 1, 27, 1, 24, 1, 35, 1, 3, 42, 108, 107, 0, 5, 25, 108, 0, 107, 12, 0, 8, 108, 25, 107, 5, 26, 108, 0, 107, 27, 0, 8, 108, 26, 107, 5, 27, 108, 0, 107, 28, 0, 0, 108, 12, 0, 9, 27, 108, 26, 9, 27, 107, 26, 0, 107, 29, 0, 0, 108, 30, 0, 9, 26, 107, 108, 0, 107, 20, 0, 9, 26, 107, 19, 38, 107, 31, 0, 0, 108, 32, 0, 28, 109, 107, 108, 5, 107, 109, 40, 107, 1264, 2, 108, 31, 0, 0, 109, 33, 0, 8, 110, 108, 109, 2, 108, 31, 0, 0, 109, 33, 0, 8, 111, 108, 109, 8, 108, 11, 111, 9, 26, 110, 108, 5, 107, 108, 0, 107, 34, 0, 9, 27, 107, 29, 0, 107, 35, 0, 9, 27, 107, 35, 0, 107, 11, 0, 9, 27, 107, 10, 0, 107, 36, 0, 9, 27, 107, 39, 0, 107, 37, 0, 9, 27, 107, 40, 0, 107, 38, 0, 9, 27, 107, 41, 0, 107, 38, 0, 47, 108, 12447, 1, 5, 1, 0, 1, 41, 9, 26, 107, 108, 0, 107, 39, 0, 47, 108, 12466, 0, 5, 0, 0, 9, 26, 107, 108, 0, 107, 40, 0, 9, 27, 107, 42, 0, 107, 40, 0, 47, 108, 12540, 1, 6, 2, 0, 1, 42, 1, 26, 9, 26, 107, 108, 2, 107, 21, 0, 0, 108, 41, 0, 0, 109, 42, 0, 44, 110, 107, 2, 108, 109, 5, 43, 110, 0, 107, 43, 0, 47, 108, 12562, 2, 9, 4, 0, 1, 44, 1, 37, 1, 32, 1, 41, 9, 26, 107, 108, 0, 107, 44, 0, 47, 108, 12789, 1, 6, 0, 0, 9, 26, 107, 108, 0, 107, 45, 0, 47, 108, 12821, 1, 7, 3, 0, 1, 44, 1, 32, 1, 41, 9, 26, 107, 108, 0, 107, 46, 0, 9, 26, 107, 45, 0, 107, 47, 0, 47, 108, 12940, 1, 8, 0, 0, 9, 26, 107, 108, 0, 107, 48, 0, 47, 108, 13014, 1, 8, 2, 0, 1, 18, 1, 32, 9, 26, 107, 108, 0, 107, 49, 0, 47, 108, 13120, 1, 6, 2, 0, 1, 37, 1, 17, 9, 26, 107, 108, 0, 107, 50, 0, 47, 108, 13213, 1, 6, 1, 0, 1, 27, 9, 26, 107, 108, 0, 107, 51, 0, 47, 108, 13245, 0, 5, 0, 0, 9, 26, 107, 108, 0, 107, 52, 0, 47, 108, 13273, 0, 5, 0, 0, 9, 26, 107, 108, 0, 107, 53, 0, 9, 26, 107, 46, 49, 107, 0, 5, 50, 107, 0, 107, 14, 0, 47, 108, 13304, 1, 9, 3, 0, 1, 54, 1, 27, 1, 13, 9, 26, 107, 108, 0, 107, 54, 0, 47, 108, 13389, 1, 7, 1, 0, 1, 55, 9, 26, 107, 108, 2, 107, 21, 0, 0, 108, 55, 0, 0, 109, 23, 0, 44, 110, 107, 2, 108, 109, 5, 56, 110, 2, 107, 21, 0, 0, 108, 56, 0, 0, 109, 23, 0, 44, 110, 107, 2, 108, 109, 5, 57, 110, 0, 107, 57, 0, 0, 108, 9, 0, 0, 109, 58, 0, 0, 110, 59, 0, 0, 111, 60, 0, 0, 112, 8, 0, 0, 113, 61, 0, 49, 114, 7, 107, 6, 108, 8, 109, 9, 110, 9, 111, 7, 112, 7, 113, 7, 5, 58, 114, 0, 107, 62, 0, 9, 27, 107, 59, 0, 107, 63, 0, 47, 108, 13476, 1, 6, 2, 0, 1, 36, 1, 24, 9, 26, 107, 108, 0, 107, 64, 0, 47, 108, 13599, 1, 7, 3, 0, 1, 54, 1, 36, 1, 32, 9, 26, 107, 108, 0, 107, 65, 0, 9, 26, 107, 62, 0, 107, 66, 0, 47, 108, 13721, 1, 8, 2, 0, 1, 54, 1, 18, 9, 26, 107, 108, 0, 107, 67, 0, 0, 108, 68, 0, 9, 27, 107, 108, 0, 107, 69, 0, 9, 27, 107, 63, 0, 107, 70, 0, 47, 108, 13798, 2, 11, 2, 0, 1, 27, 1, 63, 9, 26, 107, 108, 0, 107, 71, 0, 47, 108, 13873, 1, 9, 4, 0, 1, 55, 1, 27, 1, 63, 1, 60, 9, 26, 107, 108, 0, 107, 72, 0, 47, 108, 13942, 1, 9, 4, 0, 1, 55, 1, 27, 1, 63, 1, 60, 9, 26, 107, 108, 0, 107, 73, 0, 47, 108, 13990, 1, 9, 2, 0, 1, 27, 1, 14, 9, 26, 107, 108, 0, 107, 74, 0, 47, 108, 14104, 1, 7, 0, 0, 9, 26, 107, 108, 0, 107, 75, 0, 47, 108, 14193, 1, 9, 4, 0, 1, 55, 1, 27, 1, 63, 1, 60, 9, 26, 107, 108, 0, 107, 76, 0, 47, 108, 14304, 1, 8, 4, 0, 1, 27, 1, 63, 1, 60, 1, 24, 9, 26, 107, 108, 2, 107, 21, 0, 0, 108, 77, 0, 0, 109, 42, 0, 44, 110, 107, 2, 108, 109, 5, 64, 110, 2, 107, 21, 0, 0, 108, 78, 0, 0, 109, 79, 0, 44, 110, 107, 2, 108, 109, 5, 65, 110, 0, 107, 80, 0, 0, 108, 81, 0, 0, 109, 82, 0, 0, 110, 83, 0, 48, 111, 4, 107, 108, 109, 110, 5, 66, 111, 0, 107, 84, 0, 47, 108, 14372, 0, 9, 1, 0, 1, 69, 9, 26, 107, 108, 0, 107, 85, 0, 47, 108, 14431, 0, 7, 1, 0, 1, 69, 9, 26, 107, 108, 0, 107, 86, 0, 9, 26, 107, 70, 0, 107, 87, 0, 47, 108, 14474, 1, 8, 1, 0, 1, 69, 9, 26, 107, 108, 0, 107, 88, 0, 47, 108, 14517, 1, 6, 1, 0, 1, 27, 9, 26, 107, 108, 0, 107, 89, 0, 47, 108, 14640, 0, 6, 1, 0, 1, 69, 9, 26, 107, 108, 0, 107, 90, 0, 47, 108, 14675, 1, 8, 1, 0, 1, 27, 9, 26, 107, 108, 0, 107, 91, 0, 47, 108, 14780, 1, 6, 1, 0, 1, 27, 9, 26, 107, 108, 0, 107, 92, 0, 47, 108, 14899, 1, 11, 1, 0, 1, 69, 9, 26, 107, 108, 0, 107, 93, 0, 47, 108, 14966, 1, 7, 1, 0, 1, 69, 9, 26, 107, 108, 0, 107, 94, 0, 47, 108, 15001, 0, 9, 1, 0, 1, 69, 9, 26, 107, 108, 0, 107, 95, 0, 47, 108, 15060, 1, 11, 1, 0, 1, 69, 9, 26, 107, 108, 0, 107, 96, 0, 47, 108, 15127, 0, 7, 3, 0, 1, 27, 1, 63, 1, 60, 9, 26, 107, 108, 0, 107, 97, 0, 47, 108, 15269, 3, 11, 4, 0, 1, 55, 1, 27, 1, 63, 1, 60, 9, 26, 107, 108, 0, 107, 98, 0, 47, 108, 15319, 1, 6, 0, 0, 9, 26, 107, 108, 0, 107, 99, 0, 47, 108, 15351, 2, 7, 0, 0, 9, 26, 107, 108, 0, 107, 100, 0, 47, 108, 15384, 2, 11, 4, 0, 1, 55, 1, 27, 1, 63, 1, 60, 9, 26, 107, 108, 0, 107, 101, 0, 47, 108, 15441, 2, 6, 0, 0, 9, 26, 107, 108, 0, 107, 102, 0, 47, 108, 15466, 3, 11, 4, 0, 1, 55, 1, 27, 1, 63, 1, 60, 9, 26, 107, 108, 0, 107, 103, 0, 47, 108, 15516, 1, 6, 0, 0, 9, 26, 107, 108, 0, 107, 104, 0, 47, 108, 15548, 2, 7, 0, 0, 9, 26, 107, 108, 0, 107, 16, 0, 47, 108, 15581, 1, 11, 3, 0, 1, 27, 1, 12, 1, 15, 9, 26, 107, 108, 0, 107, 105, 0, 47, 108, 15676, 0, 5, 0, 0, 9, 26, 107, 108, 0, 107, 106, 0, 47, 108, 15738, 0, 5, 2, 0, 1, 47, 1, 4, 9, 26, 107, 108, 0, 107, 18, 0, 47, 108, 15852, 2, 8, 2, 0, 1, 27, 1, 17, 9, 26, 107, 108, 2, 107, 21, 0, 0, 108, 107, 0, 0, 109, 42, 0, 44, 110, 107, 2, 108, 109, 5, 71, 110, 0, 107, 108, 0, 47, 108, 15889, 1, 9, 2, 0, 1, 27, 1, 2, 9, 26, 107, 108, 0, 107, 109, 0, 47, 108, 16000, 0, 6, 1, 0, 1, 27, 9, 26, 107, 108, 0, 107, 110, 0, 47, 108, 16111, 0, 11, 1, 0, 1, 3, 9, 26, 107, 108, 0, 107, 111, 0, 47, 108, 16212, 0, 15, 4, 0, 1, 47, 1, 32, 1, 27, 1, 48, 9, 26, 107, 108, 0, 107, 112, 0, 0, 108, 113, 0, 0, 109, 114, 0, 0, 110, 115, 0, 0, 111, 116, 0, 0, 112, 117, 0, 0, 113, 118, 0, 0, 114, 119, 0, 0, 115, 120, 0, 0, 116, 121, 0, 0, 117, 122, 0, 0, 118, 123, 0, 0, 119, 124, 0, 0, 120, 125, 0, 0, 121, 126, 0, 0, 122, 127, 0, 0, 123, 128, 0, 0, 124, 129, 0, 49, 125, 9, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 5, 73, 125, 0, 107, 130, 0, 47, 108, 16619, 2, 10, 2, 0, 1, 36, 1, 73, 9, 26, 107, 108, 0, 107, 131, 0, 47, 108, 16794, 1, 6, 1, 0, 1, 73, 9, 26, 107, 108, 2, 107, 21, 0, 0, 108, 132, 0, 0, 109, 23, 0, 44, 110, 107, 2, 108, 109, 5, 74, 110, 49, 107, 0, 5, 76, 107, 0, 107, 133, 0, 8, 108, 6, 107, 5, 77, 108, 0, 107, 134, 0, 0, 108, 135, 0, 0, 109, 136, 0, 48, 110, 3, 107, 108, 109, 5, 78, 110, 0, 107, 137, 0, 0, 108, 30, 0, 34, 109, 108, 0, 108, 138, 0, 0, 110, 30, 0, 34, 111, 110, 0, 110, 139, 0, 0, 112, 30, 0, 34, 113, 112, 0, 112, 140, 0, 0, 114, 30, 0, 34, 115, 114, 0, 114, 141, 0, 0, 116, 30, 0, 34, 117, 116, 0, 116, 142, 0, 0, 118, 30, 0, 34, 119, 118, 0, 118, 143, 0, 0, 120, 30, 0, 34, 121, 120, 0, 120, 144, 0, 0, 122, 30, 0, 34, 123, 122, 0, 122, 145, 0, 0, 124, 30, 0, 34, 125, 124, 0, 124, 146, 0, 0, 126, 30, 0, 34, 127, 126, 0, 126, 147, 0, 0, 128, 30, 0, 34, 129, 128, 0, 128, 148, 0, 0, 130, 30, 0, 34, 131, 130, 0, 130, 149, 0, 0, 132, 30, 0, 34, 133, 132, 0, 132, 150, 0, 0, 134, 30, 0, 34, 135, 134, 0, 134, 151, 0, 0, 136, 30, 0, 34, 137, 136, 0, 136, 152, 0, 0, 138, 30, 0, 34, 139, 138, 0, 138, 153, 0, 0, 140, 30, 0, 34, 141, 140, 0, 140, 154, 0, 0, 142, 30, 0, 34, 143, 142, 49, 142, 18, 107, 109, 108, 111, 110, 113, 112, 115, 114, 117, 116, 119, 118, 121, 120, 123, 122, 125, 124, 127, 126, 129, 128, 131, 130, 133, 132, 135, 134, 137, 136, 139, 138, 141, 140, 143, 5, 80, 142, 0, 107, 155, 0, 9, 26, 107, 82, 2, 107, 21, 0, 0, 108, 156, 0, 0, 109, 23, 0, 44, 110, 107, 2, 108, 109, 5, 84, 110, 0, 107, 157, 0, 9, 26, 107, 87, 0, 107, 30, 0, 34, 108, 107, 0, 107, 68, 0, 34, 109, 107, 48, 107, 2, 108, 109, 47, 108, 16863, 2, 8, 6, 0, 1, 41, 1, 26, 1, 29, 1, 30, 1, 88, 1, 48, 42, 109, 41, 2, 107, 108, 0, 107, 158, 0, 0, 108, 159, 0, 48, 109, 2, 107, 108, 47, 107, 17414, 2, 7, 9, 0, 1, 26, 1, 37, 1, 29, 1, 30, 1, 88, 1, 49, 1, 32, 1, 47, 1, 81, 42, 108, 41, 2, 109, 107, 0, 107, 160, 0, 5, 89, 107, 0, 107, 161, 0, 47, 108, 17879, 1, 6, 6, 0, 1, 32, 1, 52, 1, 37, 1, 89, 1, 51, 1, 47, 9, 26, 107, 108, 0, 107, 162, 0, 47, 108, 18127, 0, 5, 0, 0, 9, 26, 107, 108, 0, 107, 163, 0, 47, 108, 18158, 0, 5, 0, 0, 9, 26, 107, 108, 0, 107, 164, 0, 5, 90, 107, 0, 107, 165, 0, 5, 91, 107, 0, 107, 166, 0, 0, 108, 167, 0, 0, 109, 168, 0, 0, 110, 169, 0, 49, 111, 2, 107, 108, 109, 110, 5, 92, 111, 0, 107, 170, 0, 0, 108, 171, 0, 0, 109, 172, 0, 0, 110, 173, 0, 49, 111, 2, 107, 108, 109, 110, 5, 93, 111, 2, 107, 21, 0, 0, 108, 174, 0, 0, 109, 79, 0, 44, 110, 107, 2, 108, 109, 5, 94, 110, 0, 107, 175, 0, 47, 108, 18189, 2, 14, 8, 0, 1, 36, 1, 96, 1, 95, 1, 94, 1, 2, 1, 91, 1, 92, 1, 35, 9, 26, 107, 108, 0, 107, 176, 0, 47, 108, 18606, 3, 12, 11, 0, 1, 37, 1, 32, 1, 30, 1, 29, 1, 100, 1, 36, 1, 35, 1, 41, 1, 44, 1, 96, 1, 95, 9, 26, 107, 108, 0, 107, 177, 0, 47, 108, 19034, 1, 6, 1, 0, 1, 55, 9, 26, 107, 108, 0, 107, 178, 0, 47, 108, 19079, 1, 6, 0, 0, 9, 26, 107, 108, 0, 107, 179, 0, 47, 108, 19116, 1, 6, 1, 0, 1, 27, 9, 26, 107, 108, 0, 107, 180, 0, 9, 26, 107, 101, 0, 107, 181, 0, 9, 26, 107, 102, 2, 107, 21, 0, 0, 108, 182, 0, 0, 109, 42, 0, 44, 110, 107, 2, 108, 109, 5, 103, 110, 2, 107, 21, 0, 0, 108, 183, 0, 0, 109, 79, 0, 44, 110, 107, 2, 108, 109, 5, 105, 110, 2, 107, 21, 0, 0, 108, 184, 0, 0, 109, 79, 0, 44, 110, 107, 2, 108, 109, 5, 106, 110, 0, 107, 185, 0, 47, 108, 19148, 0, 6, 7, 0, 1, 41, 1, 105, 1, 106, 1, 61, 1, 37, 1, 10, 1, 104, 9, 26, 107, 108, 38, 107, 186, 0, 0, 108, 187, 0, 22, 109, 107, 108, 40, 109, 3504, 2, 107, 188, 0, 0, 108, 186, 0, 9, 107, 108, 27, 5, 107, 27, 39, 3523, 0, 108, 189, 0, 0, 109, 190, 0, 9, 3, 109, 27, 9, 3, 108, 27, 5, 107, 27, 0, 107, 0, 0, 45, 107, 4, 3, 3, 5, 0, 42, 6, 5, 1, 1, 5, 4, 6, 34, 5, 0, 5, 6, 5, 41, 6, 3597, 34, 5, 4, 5, 7, 5, 40, 7, 3574, 3, 5, 1, 42, 8, 5, 1, 1, 34, 5, 8, 5, 7, 5, 5, 5, 7, 40, 5, 3594, 3, 7, 2, 42, 8, 7, 1, 1, 34, 7, 8, 5, 5, 7, 5, 6, 5, 40, 6, 3608, 48, 5, 0, 5, 6, 5, 39, 3788, 34, 5, 4, 5, 7, 5, 40, 7, 3637, 3, 5, 3, 0, 8, 191, 0, 8, 9, 5, 8, 43, 8, 5, 9, 1, 0, 5, 7, 8, 40, 7, 3714, 0, 5, 192, 0, 8, 7, 1, 5, 0, 5, 18, 0, 8, 8, 0, 5, 0, 5, 68, 0, 43, 9, 0, 8, 1, 5, 0, 5, 193, 0, 8, 8, 9, 5, 2, 5, 21, 0, 0, 10, 194, 0, 0, 11, 42, 0, 44, 12, 5, 2, 10, 11, 0, 5, 23, 0, 43, 10, 9, 8, 2, 12, 5, 43, 5, 1, 7, 1, 10, 5, 7, 5, 39, 3785, 34, 5, 4, 5, 8, 5, 40, 8, 3743, 3, 5, 4, 0, 9, 191, 0, 8, 10, 5, 9, 43, 9, 5, 10, 1, 0, 5, 8, 9, 40, 8, 3765, 0, 5, 195, 0, 8, 8, 1, 5, 43, 5, 1, 8, 1, 0, 5, 8, 5, 39, 3782, 0, 5, 196, 0, 8, 9, 1, 5, 43, 5, 1, 9, 1, 0, 5, 8, 5, 5, 7, 8, 5, 6, 7, 45, 6, 0, 5, 0, 0, 45, 5, 4, 2, 3, 3, 0, 31, 4, 0, 3, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 34, 3, 0, 34, 4, 3, 5, 3, 4, 40, 3, 3842, 0, 4, 2, 0, 8, 5, 0, 4, 26, 4, 0, 5, 5, 3, 4, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 34, 3, 0, 34, 4, 3, 5, 3, 4, 40, 3, 3883, 0, 4, 197, 0, 8, 5, 0, 4, 0, 4, 198, 0, 26, 6, 5, 4, 5, 3, 6, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 34, 3, 0, 34, 4, 3, 5, 3, 4, 40, 3, 3924, 0, 4, 197, 0, 8, 5, 0, 4, 0, 4, 199, 0, 26, 6, 5, 4, 5, 3, 6, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 34, 3, 0, 34, 4, 3, 5, 3, 4, 40, 3, 3965, 0, 4, 197, 0, 8, 5, 0, 4, 0, 4, 68, 0, 26, 6, 5, 4, 5, 3, 6, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 34, 3, 0, 34, 4, 3, 5, 3, 4, 40, 3, 4006, 0, 4, 197, 0, 8, 5, 0, 4, 0, 4, 200, 0, 26, 6, 5, 4, 5, 3, 6, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 36, 3, 0, 0, 4, 201, 0, 28, 5, 3, 4, 45, 5, 0, 3, 0, 0, 45, 3, 4, 2, 36, 3, 0, 0, 4, 32, 0, 28, 5, 3, 4, 45, 5, 0, 3, 0, 0, 45, 3, 4, 2, 36, 3, 0, 0, 4, 202, 0, 28, 5, 3, 4, 45, 5, 0, 3, 0, 0, 45, 3, 4, 2, 0, 3, 30, 0, 37, 4, 3, 26, 3, 0, 4, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 0, 3, 203, 0, 26, 4, 0, 3, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 2, 3, 204, 0, 2, 4, 205, 0, 42, 5, 4, 1, 0, 42, 4, 3, 1, 5, 34, 3, 4, 5, 4, 3, 40, 4, 4157, 2, 3, 206, 0, 42, 5, 3, 1, 0, 5, 4, 5, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 36, 4, 0, 0, 5, 207, 0, 29, 6, 4, 5, 5, 4, 6, 41, 4, 4195, 0, 5, 203, 0, 26, 6, 0, 5, 5, 4, 6, 40, 4, 4207, 0, 4, 68, 0, 34, 5, 4, 45, 5, 2, 4, 208, 0, 0, 5, 209, 0, 8, 6, 4, 5, 43, 5, 4, 6, 1, 0, 5, 3, 5, 0, 4, 203, 0, 26, 5, 3, 4, 5, 4, 5, 41, 4, 4261, 2, 5, 208, 0, 0, 6, 12, 0, 8, 7, 5, 6, 26, 5, 3, 7, 5, 4, 5, 45, 4, 0, 4, 0, 0, 45, 4, 4, 4, 40, 2, 4346, 0, 9, 29, 0, 8, 10, 0, 9, 5, 5, 10, 5, 9, 5, 0, 10, 68, 0, 12, 11, 5, 10, 5, 5, 11, 40, 9, 4344, 0, 9, 210, 0, 8, 10, 1, 9, 8, 9, 0, 5, 8, 11, 0, 5, 43, 12, 1, 10, 3, 9, 5, 11, 0, 9, 68, 0, 34, 10, 9, 26, 9, 12, 10, 40, 9, 4342, 45, 0, 39, 4285, 39, 4549, 3, 9, 0, 42, 10, 9, 1, 0, 40, 10, 4468, 2, 9, 208, 0, 0, 10, 211, 0, 8, 11, 9, 10, 43, 10, 9, 11, 1, 0, 5, 6, 10, 0, 9, 30, 0, 5, 5, 9, 0, 9, 29, 0, 8, 10, 6, 9, 5, 7, 10, 22, 9, 5, 7, 40, 9, 4466, 8, 9, 6, 5, 5, 8, 9, 0, 9, 210, 0, 8, 10, 1, 9, 8, 9, 0, 8, 8, 11, 0, 8, 43, 12, 1, 10, 3, 9, 8, 11, 0, 9, 68, 0, 34, 10, 9, 26, 9, 12, 10, 40, 9, 4450, 45, 0, 5, 9, 5, 0, 9, 68, 0, 11, 10, 5, 9, 5, 5, 10, 39, 4396, 39, 4549, 0, 9, 30, 0, 5, 5, 9, 0, 9, 29, 0, 8, 10, 0, 9, 5, 7, 10, 22, 9, 5, 7, 40, 9, 4549, 0, 9, 210, 0, 8, 10, 1, 9, 8, 9, 0, 5, 8, 11, 0, 5, 43, 12, 1, 10, 3, 9, 5, 11, 0, 9, 68, 0, 34, 10, 9, 26, 9, 12, 10, 40, 9, 4533, 45, 0, 5, 9, 5, 0, 9, 68, 0, 11, 10, 5, 9, 5, 5, 10, 39, 4486, 45, 0, 0, 9, 0, 0, 45, 9, 4, 1, 48, 11, 0, 5, 2, 11, 0, 11, 30, 0, 5, 3, 11, 0, 11, 29, 0, 8, 12, 0, 11, 22, 11, 3, 12, 40, 11, 4611, 8, 11, 0, 3, 9, 2, 3, 11, 5, 11, 3, 0, 11, 68, 0, 11, 12, 3, 11, 5, 3, 12, 39, 4572, 3, 11, 0, 0, 12, 30, 0, 8, 13, 2, 12, 42, 12, 11, 1, 13, 40, 12, 4648, 0, 11, 212, 0, 8, 12, 2, 11, 43, 11, 2, 12, 0, 5, 12, 11, 39, 4658, 0, 11, 68, 0, 34, 13, 11, 5, 12, 13, 5, 4, 12, 0, 11, 212, 0, 8, 12, 2, 11, 43, 11, 2, 12, 0, 5, 5, 11, 0, 11, 29, 0, 8, 12, 2, 11, 5, 6, 12, 34, 11, 5, 40, 11, 4699, 49, 11, 0, 45, 11, 34, 11, 6, 40, 11, 4720, 3, 11, 1, 3, 12, 2, 42, 13, 11, 3, 4, 12, 5, 45, 13, 0, 11, 30, 0, 5, 7, 11, 22, 11, 7, 6, 40, 11, 4921, 8, 11, 2, 7, 5, 8, 11, 52, 9, 8, 53, 11, 9, 4905, 5, 10, 11, 5, 11, 4, 40, 11, 4793, 3, 12, 3, 8, 13, 8, 10, 42, 14, 12, 1, 13, 5, 12, 14, 41, 12, 4790, 3, 13, 4, 8, 14, 8, 10, 42, 15, 13, 1, 14, 5, 12, 15, 5, 11, 12, 40, 11, 4892, 8, 11, 5, 10, 34, 12, 11, 5, 11, 12, 41, 11, 4840, 8, 12, 5, 10, 0, 13, 213, 0, 8, 14, 12, 13, 8, 12, 8, 10, 0, 13, 213, 0, 8, 15, 12, 13, 27, 12, 14, 15, 5, 11, 12, 5, 12, 11, 40, 12, 4869, 8, 11, 8, 10, 0, 13, 213, 0, 8, 14, 11, 13, 44, 11, 14, 0, 9, 5, 10, 11, 5, 12, 11, 3, 11, 1, 8, 12, 5, 10, 8, 13, 8, 10, 42, 14, 11, 3, 4, 12, 13, 5, 11, 14, 39, 4903, 8, 12, 8, 10, 9, 5, 10, 12, 5, 11, 12, 39, 4744, 5, 11, 7, 0, 11, 68, 0, 11, 12, 7, 11, 5, 7, 12, 39, 4727, 45, 5, 0, 11, 0, 0, 45, 11, 4, 2, 3, 3, 0, 42, 4, 3, 1, 0, 40, 4, 4976, 0, 3, 214, 0, 8, 4, 0, 3, 3, 3, 1, 43, 5, 0, 4, 1, 3, 5, 3, 5, 41, 3, 4971, 48, 4, 0, 5, 3, 4, 5, 4, 3, 39, 4982, 48, 3, 0, 5, 4, 3, 45, 4, 0, 3, 0, 0, 45, 3, 4, 3, 40, 0, 5240, 3, 7, 0, 42, 8, 7, 1, 0, 40, 8, 5207, 0, 7, 29, 0, 8, 8, 2, 7, 0, 7, 215, 0, 22, 9, 8, 7, 40, 9, 5126, 0, 7, 30, 0, 8, 8, 3, 7, 34, 7, 8, 5, 8, 7, 41, 8, 5064, 3, 7, 1, 0, 9, 30, 0, 8, 10, 3, 9, 42, 9, 7, 1, 10, 34, 7, 9, 5, 8, 7, 40, 8, 5073, 0, 7, 0, 0, 45, 7, 0, 7, 30, 0, 8, 8, 3, 7, 0, 7, 216, 0, 8, 9, 8, 7, 43, 7, 8, 9, 1, 0, 5, 4, 7, 3, 7, 2, 42, 8, 7, 1, 4, 40, 8, 5121, 0, 7, 30, 0, 37, 8, 7, 5, 7, 8, 39, 5124, 5, 7, 4, 45, 7, 3, 7, 3, 42, 8, 7, 1, 1, 40, 8, 5142, 5, 7, 3, 39, 5205, 3, 8, 2, 42, 9, 8, 1, 1, 40, 9, 5172, 0, 8, 45, 0, 8, 9, 3, 8, 43, 8, 3, 9, 1, 0, 5, 9, 8, 39, 5202, 0, 8, 38, 0, 8, 10, 3, 8, 47, 8, 5246, 2, 9, 3, 0, 0, 1, 1, 0, 1, 1, 43, 11, 3, 10, 1, 8, 5, 9, 11, 5, 7, 9, 45, 7, 52, 5, 0, 53, 7, 5, 5238, 5, 6, 7, 0, 7, 46, 0, 8, 8, 3, 7, 8, 7, 0, 6, 43, 9, 3, 8, 2, 6, 7, 39, 5210, 45, 3, 0, 7, 0, 0, 45, 7, 4, 3, 3, 4, 0, 42, 5, 4, 1, 1, 5, 4, 5, 40, 4, 5286, 0, 5, 217, 0, 8, 6, 1, 5, 3, 5, 1, 3, 7, 2, 43, 8, 1, 6, 2, 5, 7, 5, 4, 8, 0, 4, 0, 0, 45, 4, 4, 2, 3, 3, 0, 42, 4, 3, 1, 0, 40, 4, 5366, 0, 3, 49, 0, 8, 4, 2, 3, 43, 3, 2, 4, 0, 0, 4, 16, 0, 8, 5, 3, 4, 47, 4, 5402, 1, 6, 2, 0, 0, 1, 0, 2, 43, 6, 3, 5, 1, 4, 0, 3, 218, 0, 8, 4, 6, 3, 0, 3, 23, 0, 43, 5, 6, 4, 1, 3, 5, 3, 5, 39, 5394, 0, 4, 38, 0, 8, 5, 2, 4, 47, 4, 5460, 2, 7, 2, 0, 0, 1, 1, 0, 43, 6, 2, 5, 1, 4, 5, 3, 6, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 3, 3, 0, 42, 4, 3, 1, 0, 5, 3, 4, 41, 3, 5429, 3, 4, 1, 42, 5, 4, 1, 0, 5, 3, 5, 40, 3, 5445, 0, 3, 219, 0, 8, 4, 0, 3, 5, 3, 4, 39, 5452, 0, 4, 23, 0, 5, 3, 4, 45, 3, 0, 3, 0, 0, 45, 3, 4, 3, 3, 4, 0, 42, 5, 4, 1, 1, 5, 4, 5, 40, 4, 5490, 0, 5, 219, 0, 3, 6, 1, 9, 1, 5, 6, 5, 4, 6, 0, 4, 0, 0, 45, 4, 4, 4, 3, 6, 0, 42, 7, 6, 1, 0, 40, 7, 5602, 3, 6, 1, 0, 7, 220, 0, 8, 8, 6, 7, 0, 7, 203, 0, 43, 9, 6, 8, 2, 0, 7, 5, 5, 9, 40, 2, 5572, 0, 6, 221, 0, 8, 7, 5, 6, 43, 6, 5, 7, 1, 1, 5, 7, 6, 41, 7, 5567, 0, 6, 30, 0, 37, 8, 6, 5, 7, 8, 5, 6, 7, 39, 5600, 8, 7, 5, 1, 5, 8, 7, 41, 8, 5597, 0, 7, 133, 0, 8, 9, 0, 7, 8, 7, 9, 1, 5, 8, 7, 5, 6, 8, 45, 6, 0, 6, 0, 0, 45, 6, 4, 3, 2, 4, 222, 0, 3, 5, 0, 42, 6, 5, 2, 0, 1, 0, 5, 223, 0, 42, 7, 4, 2, 6, 5, 5, 4, 7, 41, 4, 5646, 0, 5, 30, 0, 5, 4, 5, 45, 4, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 0, 5, 224, 0, 0, 6, 13, 0, 8, 7, 5, 6, 40, 1, 5683, 0, 6, 225, 0, 5, 8, 6, 39, 5690, 0, 6, 226, 0, 5, 8, 6, 0, 6, 158, 0, 43, 9, 5, 7, 2, 8, 6, 42, 5, 4, 2, 0, 9, 3, 4, 0, 0, 6, 227, 0, 0, 7, 13, 0, 8, 8, 6, 7, 40, 1, 5734, 0, 7, 225, 0, 5, 9, 7, 39, 5741, 0, 7, 226, 0, 5, 9, 7, 43, 7, 6, 8, 1, 9, 42, 6, 4, 2, 0, 7, 11, 4, 5, 6, 3, 5, 0, 0, 6, 227, 0, 0, 7, 13, 0, 8, 8, 6, 7, 40, 1, 5784, 0, 7, 228, 0, 5, 9, 7, 39, 5791, 0, 7, 229, 0, 5, 9, 7, 43, 7, 6, 8, 1, 9, 42, 6, 5, 2, 0, 7, 11, 5, 4, 6, 3, 4, 0, 0, 6, 224, 0, 0, 7, 13, 0, 8, 8, 6, 7, 40, 1, 5834, 0, 7, 228, 0, 5, 9, 7, 39, 5841, 0, 7, 229, 0, 5, 9, 7, 0, 7, 158, 0, 43, 10, 6, 8, 2, 9, 7, 42, 6, 4, 2, 0, 10, 11, 4, 5, 6, 45, 4, 0, 4, 0, 0, 45, 4, 4, 2, 3, 5, 0, 8, 6, 5, 0, 40, 6, 5891, 3, 5, 0, 8, 6, 5, 0, 45, 6, 3, 5, 1, 42, 6, 5, 1, 0, 5, 3, 6, 3, 5, 2, 0, 6, 230, 0, 8, 7, 5, 6, 0, 5, 93, 0, 8, 6, 7, 5, 0, 5, 203, 0, 43, 8, 7, 6, 2, 3, 5, 3, 5, 3, 0, 6, 231, 0, 42, 7, 5, 2, 3, 6, 5, 4, 7, 3, 5, 2, 0, 6, 230, 0, 8, 7, 5, 6, 0, 5, 232, 0, 8, 6, 7, 5, 43, 5, 7, 6, 1, 3, 3, 5, 0, 0, 6, 233, 0, 27, 7, 4, 6, 40, 7, 5992, 5, 6, 4, 39, 5999, 0, 7, 234, 0, 5, 6, 7, 9, 5, 0, 6, 45, 6, 0, 5, 0, 0, 45, 5, 4, 2, 3, 3, 0, 0, 4, 231, 0, 42, 5, 3, 2, 0, 4, 0, 3, 233, 0, 26, 4, 5, 3, 45, 4, 0, 3, 0, 0, 45, 3, 4, 3, 5, 5, 0, 40, 5, 6095, 0, 6, 235, 0, 8, 7, 0, 6, 5, 6, 7, 41, 6, 6075, 0, 7, 236, 0, 8, 8, 0, 7, 5, 6, 8, 5, 7, 6, 41, 7, 6092, 0, 6, 237, 0, 8, 8, 0, 6, 5, 7, 8, 5, 5, 7, 5, 4, 5, 34, 5, 4, 34, 6, 5, 5, 5, 6, 40, 5, 6119, 34, 6, 1, 34, 7, 6, 5, 5, 7, 5, 6, 5, 40, 6, 6143, 0, 5, 210, 0, 8, 7, 4, 5, 43, 5, 4, 7, 2, 0, 1, 5, 6, 5, 45, 6, 0, 5, 0, 0, 45, 5, 4, 2, 3, 3, 0, 42, 4, 3, 1, 0, 40, 4, 6180, 47, 3, 6265, 2, 7, 2, 0, 0, 1, 1, 0, 5, 4, 3, 39, 6257, 3, 3, 2, 42, 5, 3, 1, 0, 40, 5, 6196, 5, 3, 0, 39, 6254, 3, 5, 3, 42, 6, 5, 1, 0, 40, 6, 6221, 47, 5, 6287, 2, 7, 1, 0, 1, 0, 5, 6, 5, 39, 6251, 40, 0, 6238, 47, 5, 6314, 2, 6, 1, 0, 1, 0, 5, 7, 5, 39, 6248, 47, 5, 6331, 0, 4, 0, 0, 5, 7, 5, 5, 6, 7, 5, 3, 6, 5, 4, 3, 45, 4, 0, 3, 0, 0, 45, 3, 4, 3, 3, 4, 0, 3, 5, 1, 42, 6, 4, 2, 1, 5, 45, 6, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 0, 5, 66, 0, 8, 6, 4, 5, 43, 5, 4, 6, 1, 1, 45, 5, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 26, 5, 1, 4, 45, 5, 0, 4, 0, 0, 45, 4, 4, 1, 0, 2, 68, 0, 34, 3, 2, 45, 3, 0, 2, 0, 0, 45, 2, 4, 3, 40, 1, 6372, 0, 4, 14, 0, 8, 5, 0, 4, 43, 4, 0, 5, 1, 1, 5, 5, 4, 39, 6375, 5, 5, 0, 45, 5, 0, 4, 0, 0, 45, 4, 4, 2, 3, 5, 0, 42, 6, 5, 1, 0, 34, 5, 6, 40, 5, 6404, 48, 5, 0, 45, 5, 3, 5, 1, 0, 6, 191, 0, 8, 7, 5, 6, 43, 6, 5, 7, 1, 0, 40, 6, 6450, 3, 5, 2, 2, 6, 21, 0, 0, 7, 238, 0, 8, 8, 6, 7, 42, 6, 5, 1, 8, 48, 5, 1, 6, 45, 5, 3, 5, 3, 0, 6, 191, 0, 8, 7, 5, 6, 43, 6, 5, 7, 1, 0, 5, 5, 6, 40, 5, 6488, 2, 6, 21, 0, 0, 7, 238, 0, 8, 8, 6, 7, 5, 5, 8, 5, 3, 5, 3, 5, 4, 8, 6, 5, 3, 5, 5, 6, 41, 5, 6518, 3, 6, 4, 0, 7, 57, 0, 8, 8, 6, 7, 5, 5, 8, 5, 4, 5, 0, 5, 239, 0, 9, 4, 5, 0, 3, 5, 5, 0, 6, 240, 0, 8, 7, 4, 6, 42, 6, 5, 1, 7, 0, 5, 54, 0, 8, 7, 6, 5, 43, 5, 6, 7, 0, 0, 6, 49, 0, 8, 7, 5, 6, 43, 6, 5, 7, 0, 45, 6, 0, 5, 0, 0, 45, 5, 4, 5, 48, 13, 0, 5, 6, 13, 3, 13, 0, 42, 14, 13, 1, 1, 5, 7, 14, 5, 13, 3, 40, 13, 6615, 3, 14, 1, 42, 15, 14, 1, 3, 5, 13, 15, 5, 8, 13, 0, 13, 30, 0, 5, 9, 13, 0, 13, 29, 0, 8, 14, 0, 13, 5, 10, 14, 22, 13, 9, 10, 40, 13, 6805, 40, 7, 6695, 8, 13, 0, 9, 42, 14, 1, 1, 13, 5, 11, 14, 0, 13, 29, 0, 8, 14, 11, 13, 5, 13, 14, 40, 13, 6693, 3, 14, 2, 0, 15, 241, 0, 8, 16, 14, 15, 43, 15, 14, 16, 2, 6, 11, 5, 13, 15, 39, 6789, 8, 13, 0, 9, 8, 14, 13, 1, 5, 12, 14, 0, 13, 203, 0, 29, 14, 12, 13, 5, 13, 14, 40, 13, 6748, 5, 14, 3, 40, 14, 6742, 0, 15, 68, 0, 32, 16, 15, 42, 15, 8, 2, 16, 12, 5, 14, 15, 34, 15, 14, 5, 13, 15, 40, 13, 6789, 0, 13, 17, 0, 8, 14, 6, 13, 43, 13, 6, 14, 1, 12, 40, 2, 6777, 8, 13, 12, 1, 5, 14, 13, 39, 6784, 0, 13, 203, 0, 5, 14, 13, 5, 12, 14, 39, 6706, 5, 13, 9, 0, 13, 68, 0, 11, 14, 9, 13, 5, 9, 14, 39, 6636, 45, 6, 0, 13, 0, 0, 45, 13, 4, 2, 0, 3, 242, 0, 8, 4, 0, 3, 5, 3, 4, 40, 3, 6840, 0, 4, 243, 0, 8, 5, 0, 4, 5, 3, 5, 40, 3, 6894, 3, 3, 0, 3, 4, 1, 0, 5, 210, 0, 8, 6, 4, 5, 0, 5, 243, 0, 8, 7, 0, 5, 47, 5, 6926, 1, 7, 0, 0, 43, 8, 4, 6, 2, 7, 5, 0, 4, 244, 0, 42, 5, 3, 2, 8, 4, 5, 3, 5, 39, 6918, 0, 4, 244, 0, 8, 5, 0, 4, 5, 4, 5, 41, 4, 6915, 0, 5, 23, 0, 5, 4, 5, 5, 3, 4, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 0, 3, 245, 0, 8, 4, 0, 3, 5, 3, 4, 40, 3, 6956, 0, 4, 246, 0, 8, 5, 0, 4, 34, 4, 5, 5, 3, 4, 5, 4, 3, 40, 4, 6984, 0, 3, 247, 0, 8, 5, 0, 3, 0, 3, 246, 0, 8, 6, 5, 3, 34, 3, 6, 5, 4, 3, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 0, 3, 29, 0, 8, 4, 1, 3, 40, 4, 7045, 0, 3, 38, 0, 8, 4, 2, 3, 47, 3, 7089, 2, 11, 7, 0, 0, 0, 0, 1, 1, 0, 0, 2, 0, 3, 0, 4, 0, 5, 43, 5, 2, 4, 1, 3, 5, 3, 5, 39, 7081, 0, 4, 30, 0, 8, 5, 2, 4, 5, 4, 5, 40, 4, 7078, 3, 5, 6, 0, 6, 30, 0, 8, 7, 2, 6, 42, 6, 5, 1, 7, 5, 4, 6, 5, 3, 4, 45, 3, 0, 3, 0, 0, 45, 3, 4, 3, 0, 6, 242, 0, 8, 7, 1, 6, 5, 6, 7, 40, 6, 7116, 0, 7, 243, 0, 8, 8, 1, 7, 5, 6, 8, 5, 4, 6, 5, 6, 4, 41, 6, 7153, 3, 7, 0, 0, 8, 191, 0, 8, 9, 7, 8, 0, 8, 80, 0, 8, 10, 1, 8, 43, 8, 7, 9, 1, 10, 5, 6, 8, 40, 6, 7332, 3, 6, 1, 3, 7, 2, 42, 8, 6, 1, 7, 40, 8, 7200, 3, 6, 3, 0, 7, 210, 0, 8, 8, 6, 7, 3, 7, 2, 2, 9, 248, 0, 43, 10, 6, 8, 2, 7, 9, 5, 6, 10, 39, 7244, 3, 7, 4, 3, 8, 2, 42, 9, 7, 1, 8, 40, 9, 7222, 48, 7, 0, 5, 8, 7, 39, 7241, 2, 7, 248, 0, 3, 9, 2, 42, 10, 7, 1, 9, 48, 7, 1, 10, 5, 8, 7, 5, 6, 8, 5, 5, 6, 40, 4, 7289, 3, 6, 5, 0, 7, 243, 0, 8, 8, 1, 7, 47, 7, 7395, 2, 9, 1, 0, 1, 5, 0, 9, 30, 0, 34, 10, 9, 42, 9, 6, 3, 8, 7, 10, 5, 6, 9, 39, 7330, 0, 7, 249, 0, 0, 8, 15, 0, 8, 9, 5, 8, 0, 8, 244, 0, 8, 10, 1, 8, 43, 8, 5, 9, 1, 10, 0, 9, 30, 0, 25, 10, 8, 9, 9, 1, 7, 10, 5, 6, 10, 39, 7389, 0, 6, 244, 0, 3, 7, 6, 3, 8, 2, 42, 9, 7, 1, 8, 5, 7, 9, 41, 7, 7367, 3, 8, 4, 3, 9, 2, 42, 10, 8, 1, 9, 5, 7, 10, 40, 7, 7379, 0, 7, 23, 0, 5, 8, 7, 39, 7385, 3, 7, 2, 5, 8, 7, 9, 1, 6, 8, 0, 6, 0, 0, 45, 6, 4, 3, 0, 4, 245, 0, 3, 5, 0, 0, 6, 15, 0, 8, 7, 5, 6, 0, 6, 244, 0, 8, 8, 1, 6, 43, 6, 5, 7, 1, 8, 0, 5, 30, 0, 25, 7, 6, 5, 9, 1, 4, 7, 0, 4, 0, 0, 45, 4, 4, 2, 0, 3, 29, 0, 8, 4, 0, 3, 0, 3, 68, 0, 23, 5, 4, 3, 40, 5, 7497, 3, 3, 0, 0, 4, 210, 0, 8, 5, 3, 4, 47, 4, 7508, 3, 8, 1, 0, 0, 1, 43, 6, 3, 5, 2, 0, 4, 5, 3, 6, 39, 7500, 5, 3, 0, 45, 3, 0, 3, 0, 0, 45, 3, 4, 4, 3, 5, 0, 0, 6, 210, 0, 8, 7, 5, 6, 43, 6, 5, 7, 2, 2, 0, 26, 5, 6, 1, 45, 5, 0, 5, 0, 0, 45, 5, 4, 3, 3, 5, 0, 42, 6, 5, 1, 0, 5, 4, 6, 0, 5, 14, 0, 8, 6, 4, 5, 0, 5, 250, 0, 43, 7, 4, 6, 1, 5, 0, 5, 70, 0, 8, 6, 7, 5, 0, 5, 76, 0, 8, 8, 4, 5, 0, 5, 250, 0, 43, 9, 4, 8, 1, 5, 43, 5, 7, 6, 1, 9, 0, 6, 38, 0, 8, 7, 5, 6, 47, 6, 7644, 2, 11, 7, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 1, 1, 43, 8, 5, 7, 1, 6, 0, 5, 0, 0, 45, 5, 4, 3, 3, 5, 0, 0, 6, 191, 0, 8, 7, 5, 6, 0, 6, 80, 0, 8, 8, 1, 6, 43, 6, 5, 7, 1, 8, 5, 5, 6, 40, 5, 7697, 3, 6, 1, 0, 7, 251, 0, 8, 8, 6, 7, 43, 7, 6, 8, 1, 1, 5, 5, 7, 40, 5, 7831, 3, 5, 2, 0, 6, 250, 0, 42, 7, 5, 1, 6, 5, 4, 7, 0, 5, 53, 0, 0, 6, 219, 0, 8, 7, 1, 6, 0, 6, 193, 0, 8, 8, 7, 6, 3, 6, 3, 0, 9, 23, 0, 43, 10, 7, 8, 2, 6, 9, 9, 4, 5, 10, 3, 5, 4, 3, 6, 5, 47, 7, 7837, 2, 8, 2, 0, 1, 1, 1, 4, 42, 8, 5, 2, 6, 7, 3, 5, 6, 0, 6, 252, 0, 8, 7, 5, 6, 0, 5, 93, 0, 8, 6, 7, 5, 0, 5, 203, 0, 43, 8, 7, 6, 2, 4, 5, 3, 5, 6, 0, 6, 252, 0, 8, 7, 5, 6, 0, 5, 232, 0, 8, 6, 7, 5, 43, 5, 7, 6, 1, 4, 0, 5, 0, 0, 45, 5, 4, 3, 3, 4, 0, 8, 5, 4, 1, 5, 4, 5, 40, 4, 7869, 3, 5, 1, 3, 6, 0, 8, 7, 6, 1, 9, 5, 1, 7, 5, 4, 7, 0, 4, 0, 0, 45, 4, 4, 6, 40, 3, 7923, 0, 7, 93, 0, 8, 8, 0, 7, 40, 2, 7904, 0, 7, 253, 0, 8, 9, 0, 7, 5, 7, 9, 39, 7911, 0, 9, 203, 0, 5, 7, 9, 43, 9, 0, 8, 2, 1, 7, 5, 7, 9, 39, 8018, 0, 8, 254, 0, 8, 9, 0, 8, 0, 8, 255, 0, 26, 10, 9, 8, 40, 10, 7970, 0, 8, 247, 0, 8, 9, 0, 8, 0, 8, 256, 0, 8, 10, 9, 8, 43, 8, 9, 10, 2, 1, 0, 5, 9, 8, 39, 8015, 0, 8, 247, 0, 8, 10, 0, 8, 0, 8, 93, 0, 8, 11, 10, 8, 40, 2, 7994, 5, 8, 0, 39, 8005, 0, 12, 257, 0, 8, 13, 0, 12, 5, 8, 13, 43, 12, 10, 11, 2, 1, 8, 5, 9, 12, 5, 7, 9, 5, 7, 4, 40, 7, 8044, 3, 8, 0, 0, 9, 258, 0, 8, 10, 0, 9, 42, 9, 8, 2, 1, 10, 5, 7, 9, 0, 7, 0, 0, 45, 7, 4, 9, 3, 10, 0, 47, 11, 8095, 2, 9, 9, 0, 0, 0, 0, 1, 1, 1, 1, 2, 0, 2, 1, 3, 1, 4, 1, 7, 1, 6, 42, 12, 10, 3, 0, 11, 5, 45, 1, 0, 10, 0, 0, 45, 10, 4, 3, 3, 4, 0, 3, 5, 1, 42, 6, 5, 1, 1, 47, 5, 8147, 2, 9, 8, 0, 0, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 3, 7, 8, 42, 8, 4, 3, 6, 5, 7, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 3, 5, 1, 3, 6, 2, 42, 7, 5, 1, 6, 47, 5, 8198, 2, 13, 6, 0, 0, 3, 1, 1, 1, 0, 0, 4, 0, 5, 0, 6, 3, 6, 7, 42, 8, 4, 3, 7, 5, 6, 0, 4, 0, 0, 45, 4, 4, 3, 3, 7, 0, 40, 7, 8214, 3, 7, 1, 5, 8, 7, 39, 8217, 5, 8, 1, 5, 4, 8, 3, 7, 0, 40, 7, 8231, 5, 7, 1, 39, 8237, 3, 8, 1, 5, 7, 8, 5, 5, 7, 3, 7, 0, 40, 7, 8254, 3, 7, 2, 5, 8, 7, 39, 8257, 5, 8, 0, 5, 6, 8, 3, 7, 3, 40, 6, 8292, 0, 8, 259, 0, 8, 9, 5, 8, 0, 8, 30, 0, 34, 10, 8, 43, 8, 5, 9, 1, 10, 5, 9, 8, 39, 8295, 5, 9, 5, 3, 8, 4, 3, 10, 5, 34, 11, 6, 42, 12, 7, 5, 4, 9, 8, 10, 11, 0, 7, 0, 0, 45, 7, 4, 2, 0, 4, 29, 0, 8, 5, 1, 4, 34, 4, 5, 40, 4, 8370, 0, 4, 30, 0, 8, 5, 2, 4, 5, 4, 5, 40, 4, 8368, 0, 5, 30, 0, 8, 6, 2, 5, 0, 5, 239, 0, 8, 7, 6, 5, 5, 4, 7, 45, 4, 3, 4, 0, 42, 5, 4, 1, 0, 40, 5, 8383, 45, 2, 2, 4, 21, 0, 0, 5, 260, 0, 0, 6, 23, 0, 44, 7, 4, 2, 5, 6, 0, 4, 191, 0, 8, 5, 7, 4, 43, 4, 7, 5, 1, 0, 5, 3, 4, 0, 4, 38, 0, 8, 5, 2, 4, 47, 4, 8455, 2, 9, 4, 0, 0, 1, 1, 3, 0, 2, 1, 0, 43, 6, 2, 5, 1, 4, 45, 6, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 42, 5, 4, 1, 1, 5, 4, 5, 40, 4, 8537, 3, 5, 1, 40, 5, 8520, 3, 5, 2, 42, 6, 5, 1, 1, 0, 5, 39, 0, 8, 7, 6, 5, 43, 5, 6, 7, 0, 0, 6, 85, 0, 8, 7, 5, 6, 3, 6, 3, 43, 8, 5, 7, 1, 6, 5, 5, 8, 39, 8534, 0, 6, 239, 0, 3, 7, 3, 9, 1, 6, 7, 5, 5, 7, 5, 4, 5, 0, 4, 0, 0, 45, 4, 4, 2, 0, 3, 193, 0, 8, 4, 0, 3, 3, 3, 0, 47, 5, 8578, 2, 6, 0, 0, 43, 6, 0, 4, 2, 3, 5, 45, 6, 0, 3, 0, 0, 45, 3, 4, 3, 0, 4, 261, 0, 8, 5, 1, 4, 43, 4, 1, 5, 0, 45, 4, 0, 4, 0, 0, 45, 4, 4, 2, 3, 3, 0, 0, 4, 191, 0, 8, 5, 3, 4, 43, 4, 3, 5, 1, 0, 45, 4, 0, 3, 0, 0, 45, 3, 4, 3, 0, 7, 30, 0, 37, 8, 7, 26, 7, 1, 8, 5, 8, 7, 40, 8, 8661, 3, 7, 0, 42, 9, 7, 1, 0, 5, 1, 9, 5, 8, 1, 40, 1, 8666, 45, 0, 3, 7, 1, 8, 8, 7, 0, 34, 7, 8, 40, 7, 8898, 3, 7, 2, 42, 8, 7, 1, 0, 5, 4, 8, 0, 7, 23, 0, 0, 8, 13, 0, 8, 9, 7, 8, 0, 8, 30, 0, 8, 10, 4, 8, 0, 8, 261, 0, 8, 11, 10, 8, 43, 8, 10, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 13, 0, 8, 8, 10, 7, 0, 7, 18, 0, 8, 9, 4, 7, 0, 7, 68, 0, 43, 11, 4, 9, 1, 7, 43, 7, 10, 8, 1, 11, 5, 5, 7, 0, 7, 23, 0, 0, 8, 13, 0, 8, 9, 7, 8, 0, 8, 262, 0, 43, 10, 7, 9, 2, 4, 8, 0, 7, 13, 0, 8, 8, 10, 7, 3, 7, 3, 0, 9, 218, 0, 8, 11, 7, 9, 0, 9, 23, 0, 0, 12, 13, 0, 8, 13, 9, 12, 0, 12, 262, 0, 43, 14, 9, 13, 2, 5, 12, 43, 9, 7, 11, 1, 14, 43, 7, 10, 8, 1, 9, 0, 8, 13, 0, 8, 9, 7, 8, 43, 8, 7, 9, 1, 5, 0, 7, 263, 0, 8, 9, 8, 7, 0, 7, 262, 0, 43, 10, 8, 9, 1, 7, 5, 6, 10, 3, 7, 4, 47, 8, 8913, 2, 6, 3, 0, 0, 5, 0, 1, 1, 0, 42, 9, 7, 2, 6, 8, 3, 7, 1, 8, 8, 7, 0, 45, 8, 0, 7, 0, 0, 45, 7, 4, 3, 3, 4, 0, 30, 5, 1, 4, 40, 5, 8944, 3, 4, 1, 3, 5, 2, 9, 4, 5, 1, 0, 4, 68, 0, 34, 5, 4, 45, 5, 0, 4, 0, 0, 45, 4, 4, 4, 0, 5, 30, 0, 37, 6, 5, 26, 5, 2, 6, 5, 6, 5, 40, 6, 8983, 3, 5, 0, 42, 7, 5, 1, 0, 5, 2, 7, 5, 6, 2, 34, 5, 2, 5, 6, 5, 40, 6, 9005, 3, 5, 1, 8, 7, 5, 0, 34, 5, 7, 5, 6, 5, 5, 5, 6, 40, 5, 9022, 3, 6, 2, 42, 7, 6, 1, 1, 5, 5, 7, 40, 5, 9053, 0, 5, 23, 0, 0, 6, 13, 0, 8, 7, 5, 6, 0, 6, 264, 0, 43, 8, 5, 7, 2, 1, 6, 5, 5, 8, 39, 9056, 5, 5, 1, 45, 5, 0, 5, 0, 0, 45, 5, 4, 3, 3, 7, 0, 42, 8, 7, 1, 0, 40, 8, 9217, 3, 7, 1, 42, 8, 7, 1, 0, 5, 4, 8, 3, 7, 2, 42, 8, 7, 2, 0, 4, 5, 0, 8, 0, 7, 29, 0, 8, 8, 2, 7, 0, 7, 215, 0, 22, 9, 8, 7, 40, 9, 9159, 0, 7, 30, 0, 8, 8, 3, 7, 5, 7, 8, 40, 7, 9154, 3, 8, 3, 0, 9, 30, 0, 8, 10, 3, 9, 42, 9, 8, 3, 10, 0, 4, 5, 7, 9, 5, 8, 7, 39, 9215, 40, 0, 9209, 3, 7, 4, 42, 9, 7, 3, 0, 1, 4, 5, 1, 9, 0, 7, 38, 0, 8, 9, 3, 7, 47, 7, 9256, 2, 10, 4, 0, 0, 5, 1, 4, 1, 0, 1, 1, 43, 10, 3, 9, 1, 7, 5, 7, 10, 39, 9212, 5, 7, 3, 5, 8, 7, 45, 8, 52, 5, 0, 53, 7, 5, 9248, 5, 6, 7, 0, 7, 155, 0, 8, 8, 3, 7, 8, 7, 0, 6, 43, 9, 3, 8, 2, 6, 7, 39, 9220, 45, 3, 0, 7, 0, 0, 45, 7, 4, 3, 3, 4, 0, 42, 5, 4, 1, 1, 5, 4, 5, 40, 4, 9336, 3, 5, 1, 40, 5, 9312, 0, 5, 133, 0, 8, 6, 1, 5, 0, 5, 265, 0, 8, 7, 6, 5, 3, 5, 2, 3, 8, 3, 43, 9, 6, 7, 2, 5, 8, 5, 5, 9, 39, 9333, 0, 6, 133, 0, 8, 7, 1, 6, 3, 6, 2, 3, 8, 3, 9, 7, 6, 8, 5, 5, 8, 5, 4, 5, 0, 4, 0, 0, 45, 4, 4, 3, 54, 9358, 4, 42, 5, 0, 1, 1, 55, 45, 5, 55, 39, 9360, 45, 1, 0, 5, 0, 0, 45, 5, 4, 3, 0, 5, 266, 0, 8, 6, 0, 5, 8, 5, 6, 1, 5, 6, 5, 41, 6, 9409, 0, 5, 266, 0, 8, 7, 0, 5, 3, 5, 0, 42, 8, 5, 1, 1, 8, 5, 7, 8, 5, 6, 5, 5, 4, 6, 3, 5, 1, 0, 6, 191, 0, 8, 7, 5, 6, 43, 6, 5, 7, 1, 4, 40, 6, 9437, 5, 5, 4, 39, 9461, 3, 6, 2, 2, 7, 267, 0, 0, 8, 268, 0, 8, 9, 7, 8, 42, 7, 6, 2, 9, 4, 5, 5, 7, 45, 5, 0, 5, 0, 0, 45, 5, 4, 4, 3, 5, 0, 2, 6, 267, 0, 0, 7, 269, 0, 8, 8, 6, 7, 42, 6, 5, 2, 8, 2, 5, 2, 6, 0, 5, 266, 0, 8, 6, 0, 5, 3, 5, 1, 42, 7, 5, 1, 1, 9, 6, 7, 2, 0, 5, 0, 0, 45, 5, 4, 3, 34, 8, 0, 40, 8, 9606, 0, 8, 30, 0, 8, 9, 3, 8, 34, 8, 9, 40, 8, 9549, 0, 8, 0, 0, 45, 8, 49, 8, 0, 5, 4, 8, 0, 8, 30, 0, 8, 9, 3, 8, 0, 8, 266, 0, 8, 10, 9, 8, 52, 5, 10, 53, 8, 5, 9604, 5, 6, 8, 3, 8, 0, 0, 9, 30, 0, 8, 10, 3, 9, 42, 9, 8, 2, 10, 6, 9, 4, 6, 9, 39, 9574, 45, 4, 3, 8, 1, 42, 9, 8, 1, 0, 40, 9, 9726, 0, 8, 29, 0, 8, 9, 2, 8, 0, 8, 215, 0, 22, 10, 9, 8, 40, 10, 9675, 0, 8, 30, 0, 8, 9, 3, 8, 5, 8, 9, 40, 8, 9670, 3, 9, 0, 0, 10, 30, 0, 8, 11, 3, 10, 42, 10, 9, 2, 11, 0, 5, 8, 10, 5, 9, 8, 39, 9724, 3, 8, 2, 42, 10, 8, 1, 1, 40, 10, 9691, 5, 8, 3, 39, 9721, 0, 10, 38, 0, 8, 11, 3, 10, 47, 10, 9765, 2, 8, 3, 0, 0, 3, 1, 0, 1, 1, 43, 12, 3, 11, 1, 10, 5, 8, 12, 5, 9, 8, 45, 9, 52, 7, 0, 53, 8, 7, 9757, 5, 6, 8, 0, 8, 157, 0, 8, 9, 3, 8, 8, 8, 0, 6, 43, 10, 3, 9, 2, 6, 8, 39, 9729, 45, 3, 0, 8, 0, 0, 45, 8, 4, 3, 3, 4, 0, 3, 5, 1, 3, 6, 2, 42, 7, 4, 3, 1, 5, 6, 0, 4, 0, 0, 45, 4, 4, 3, 0, 5, 3, 0, 8, 6, 0, 5, 5, 4, 6, 2, 5, 270, 0, 0, 6, 271, 0, 8, 7, 5, 6, 0, 6, 230, 0, 8, 8, 0, 6, 0, 6, 272, 0, 0, 9, 13, 0, 8, 10, 6, 9, 43, 9, 6, 10, 1, 1, 8, 6, 8, 9, 0, 8, 272, 0, 0, 9, 13, 0, 8, 10, 8, 9, 43, 9, 8, 10, 1, 1, 8, 8, 4, 9, 0, 9, 230, 0, 8, 10, 0, 9, 0, 9, 110, 0, 0, 11, 13, 0, 8, 12, 9, 11, 43, 11, 9, 12, 1, 1, 8, 9, 10, 11, 0, 10, 110, 0, 0, 11, 13, 0, 8, 12, 10, 11, 43, 11, 10, 12, 1, 1, 8, 10, 4, 11, 0, 11, 273, 0, 0, 12, 13, 0, 8, 13, 11, 12, 43, 12, 11, 13, 1, 1, 8, 11, 4, 12, 43, 12, 5, 7, 5, 6, 8, 9, 10, 11, 45, 12, 0, 5, 0, 0, 45, 5, 4, 2, 3, 3, 0, 8, 4, 3, 0, 5, 3, 4, 41, 3, 9983, 3, 4, 1, 8, 5, 4, 0, 5, 3, 5, 5, 4, 3, 41, 4, 9992, 5, 4, 0, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 0, 4, 263, 0, 8, 5, 0, 4, 3, 4, 0, 43, 6, 0, 5, 1, 4, 5, 3, 6, 0, 4, 30, 0, 8, 5, 3, 4, 0, 4, 18, 0, 8, 6, 3, 4, 0, 4, 68, 0, 43, 7, 3, 6, 1, 4, 0, 4, 274, 0, 8, 6, 7, 4, 43, 4, 7, 6, 0, 48, 6, 2, 5, 4, 45, 6, 0, 4, 0, 0, 45, 4, 4, 2, 3, 3, 0, 3, 4, 0, 8, 5, 0, 4, 5, 4, 5, 41, 4, 10098, 49, 5, 0, 5, 4, 5, 9, 0, 3, 4, 45, 4, 0, 3, 0, 0, 45, 3, 4, 6, 3, 8, 0, 42, 9, 8, 1, 0, 5, 7, 9, 8, 8, 7, 1, 5, 9, 8, 41, 9, 10139, 48, 8, 0, 5, 9, 8, 9, 7, 1, 9, 8, 8, 7, 1, 0, 9, 17, 0, 8, 10, 8, 9, 48, 9, 3, 2, 3, 4, 43, 11, 8, 10, 1, 9, 0, 8, 275, 0, 8, 9, 0, 8, 43, 8, 0, 9, 2, 1, 4, 0, 8, 0, 0, 45, 8, 4, 3, 34, 4, 1, 5, 5, 4, 41, 5, 10232, 3, 4, 0, 0, 6, 210, 0, 8, 7, 4, 6, 47, 6, 10240, 1, 6, 1, 0, 1, 0, 43, 8, 4, 7, 2, 1, 6, 34, 4, 8, 5, 5, 4, 45, 5, 0, 4, 0, 0, 45, 4, 4, 2, 3, 3, 0, 0, 4, 15, 0, 8, 5, 3, 4, 43, 4, 3, 5, 1, 0, 0, 3, 30, 0, 22, 5, 4, 3, 45, 5, 0, 3, 0, 0, 45, 3, 4, 6, 3, 9, 0, 42, 10, 9, 1, 0, 5, 7, 10, 40, 1, 10347, 8, 9, 7, 1, 5, 10, 9, 40, 10, 10345, 8, 9, 7, 1, 0, 11, 14, 0, 8, 12, 9, 11, 47, 11, 10377, 1, 11, 6, 0, 1, 4, 0, 1, 1, 2, 1, 3, 1, 0, 1, 1, 43, 13, 9, 12, 1, 11, 9, 7, 1, 13, 5, 10, 13, 39, 10371, 52, 8, 7, 53, 9, 8, 10371, 5, 1, 9, 3, 9, 2, 42, 10, 9, 5, 0, 1, 2, 3, 4, 39, 10350, 0, 9, 0, 0, 45, 9, 4, 2, 0, 6, 30, 0, 8, 7, 0, 6, 5, 3, 7, 0, 6, 68, 0, 8, 7, 0, 6, 5, 4, 7, 0, 6, 215, 0, 8, 7, 0, 6, 5, 5, 7, 3, 6, 0, 5, 7, 6, 40, 7, 10447, 0, 6, 67, 0, 8, 8, 5, 6, 3, 6, 0, 0, 9, 67, 0, 8, 10, 6, 9, 27, 6, 8, 10, 5, 7, 6, 5, 6, 7, 41, 6, 10471, 3, 7, 1, 3, 8, 2, 42, 9, 7, 2, 3, 8, 34, 7, 9, 5, 6, 7, 5, 7, 6, 41, 7, 10499, 3, 6, 3, 5, 8, 6, 40, 8, 10496, 3, 6, 3, 27, 9, 6, 4, 5, 8, 9, 5, 7, 8, 40, 7, 10511, 0, 6, 30, 0, 34, 7, 6, 45, 7, 3, 6, 4, 0, 7, 276, 0, 8, 8, 6, 7, 3, 7, 5, 43, 9, 6, 8, 2, 7, 5, 0, 6, 0, 0, 45, 6, 4, 6, 5, 7, 6, 3, 10, 0, 42, 11, 10, 1, 0, 34, 10, 11, 40, 10, 10593, 52, 8, 0, 53, 10, 8, 10591, 5, 9, 10, 0, 10, 180, 0, 8, 11, 6, 10, 8, 10, 0, 9, 43, 12, 6, 11, 5, 9, 1, 2, 10, 4, 39, 10560, 45, 6, 3, 10, 0, 42, 11, 10, 1, 1, 5, 10, 11, 41, 10, 10695, 3, 11, 1, 42, 12, 11, 1, 1, 5, 11, 12, 41, 11, 10632, 3, 12, 2, 42, 13, 12, 1, 1, 5, 11, 13, 40, 11, 10647, 0, 11, 23, 0, 5, 1, 11, 5, 11, 1, 39, 10692, 3, 12, 1, 42, 13, 12, 1, 2, 40, 13, 10673, 5, 2, 1, 0, 12, 23, 0, 5, 1, 12, 5, 12, 1, 39, 10689, 5, 3, 2, 5, 2, 1, 0, 13, 23, 0, 5, 1, 13, 5, 12, 1, 5, 11, 12, 5, 10, 11, 3, 10, 3, 42, 11, 10, 1, 3, 5, 10, 11, 41, 10, 10725, 5, 3, 2, 0, 11, 30, 0, 37, 12, 11, 5, 2, 12, 5, 10, 2, 40, 3, 10793, 3, 10, 4, 3, 11, 5, 42, 12, 11, 1, 0, 47, 11, 10804, 2, 15, 18, 0, 0, 6, 0, 7, 0, 8, 0, 9, 1, 7, 0, 10, 0, 11, 0, 12, 0, 13, 0, 14, 1, 1, 0, 15, 1, 2, 1, 3, 1, 4, 0, 16, 0, 17, 0, 18, 42, 13, 10, 2, 12, 11, 5, 10, 6, 39, 10796, 5, 10, 6, 45, 10, 0, 10, 0, 0, 45, 10, 4, 3, 3, 10, 0, 42, 11, 10, 1, 1, 5, 4, 11, 0, 10, 30, 0, 8, 11, 4, 10, 5, 5, 11, 0, 10, 68, 0, 8, 11, 4, 10, 5, 6, 11, 3, 10, 1, 42, 11, 10, 1, 5, 5, 7, 11, 3, 10, 2, 30, 11, 5, 10, 5, 8, 11, 3, 10, 3, 30, 11, 5, 10, 5, 9, 11, 5, 10, 7, 40, 10, 10937, 3, 11, 4, 0, 12, 38, 0, 8, 13, 11, 12, 47, 12, 10943, 2, 15, 17, 0, 0, 5, 0, 6, 0, 7, 0, 8, 1, 6, 0, 9, 0, 10, 1, 9, 1, 7, 1, 8, 0, 11, 0, 12, 0, 13, 0, 14, 0, 15, 0, 16, 0, 17, 43, 14, 11, 13, 1, 12, 5, 10, 14, 0, 10, 0, 0, 45, 10, 4, 3, 3, 5, 0, 42, 6, 5, 1, 1, 34, 5, 6, 5, 6, 5, 40, 6, 10976, 3, 5, 1, 42, 7, 5, 1, 1, 34, 5, 7, 5, 6, 5, 5, 5, 6, 40, 5, 10996, 3, 6, 2, 42, 7, 6, 1, 1, 34, 6, 7, 5, 5, 6, 34, 6, 5, 40, 6, 11126, 47, 5, 11132, 1, 14, 14, 0, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 1, 1, 0, 8, 0, 9, 0, 10, 0, 11, 0, 12, 0, 13, 0, 14, 1, 4, 5, 4, 5, 0, 5, 67, 0, 3, 6, 12, 0, 7, 67, 0, 3, 8, 12, 0, 9, 67, 0, 8, 10, 8, 9, 5, 8, 10, 41, 8, 11097, 3, 9, 15, 0, 10, 67, 0, 8, 11, 9, 10, 5, 12, 11, 0, 13, 68, 0, 11, 14, 11, 13, 9, 9, 10, 14, 5, 8, 12, 9, 6, 7, 8, 9, 4, 5, 8, 3, 5, 16, 3, 6, 8, 3, 7, 4, 3, 8, 6, 42, 9, 5, 5, 1, 6, 7, 8, 4, 0, 5, 0, 0, 45, 5, 4, 2, 0, 6, 277, 0, 8, 7, 0, 6, 0, 6, 278, 0, 0, 8, 13, 0, 8, 9, 6, 8, 0, 8, 80, 0, 8, 10, 0, 8, 43, 8, 6, 9, 1, 10, 8, 6, 7, 8, 40, 6, 11190, 0, 6, 279, 0, 8, 7, 0, 6, 43, 6, 0, 7, 0, 45, 6, 0, 6, 280, 0, 8, 7, 0, 6, 5, 6, 7, 40, 6, 11247, 3, 7, 0, 3, 8, 1, 0, 9, 280, 0, 8, 10, 0, 9, 0, 9, 263, 0, 8, 11, 10, 9, 3, 9, 2, 43, 12, 10, 11, 1, 9, 42, 9, 7, 2, 8, 12, 34, 7, 9, 5, 6, 7, 34, 7, 6, 5, 6, 7, 40, 6, 11391, 3, 7, 3, 34, 8, 7, 5, 7, 8, 40, 7, 11385, 3, 8, 4, 5, 9, 8, 40, 9, 11319, 0, 8, 277, 0, 8, 10, 0, 8, 3, 8, 5, 27, 11, 10, 8, 5, 8, 11, 41, 8, 11316, 0, 10, 281, 0, 8, 11, 0, 10, 3, 10, 6, 26, 12, 11, 10, 5, 8, 12, 5, 9, 8, 5, 8, 9, 41, 8, 11382, 3, 9, 7, 5, 10, 9, 40, 10, 11345, 0, 9, 282, 0, 8, 11, 0, 9, 5, 10, 11, 5, 9, 10, 40, 9, 11379, 3, 10, 5, 0, 11, 251, 0, 8, 12, 10, 11, 0, 11, 282, 0, 8, 13, 0, 11, 43, 11, 10, 12, 1, 13, 5, 9, 11, 5, 8, 9, 5, 7, 8, 34, 8, 7, 5, 6, 8, 40, 6, 11759, 3, 6, 5, 5, 3, 6, 3, 6, 3, 40, 6, 11479, 0, 6, 277, 0, 8, 7, 0, 6, 5, 4, 7, 3, 6, 8, 3, 7, 3, 42, 8, 6, 2, 4, 7, 34, 6, 8, 40, 6, 11476, 3, 6, 5, 26, 7, 4, 6, 5, 6, 7, 41, 6, 11465, 0, 7, 247, 0, 8, 8, 4, 7, 5, 4, 8, 34, 7, 4, 5, 6, 7, 40, 6, 11474, 0, 6, 0, 0, 45, 6, 39, 11417, 5, 3, 4, 2, 6, 208, 0, 0, 7, 283, 0, 8, 8, 6, 7, 0, 7, 284, 0, 0, 9, 285, 0, 0, 10, 30, 0, 34, 11, 10, 0, 10, 49, 0, 47, 12, 11765, 0, 3, 1, 0, 1, 3, 49, 13, 2, 9, 11, 10, 12, 43, 9, 6, 8, 3, 0, 7, 13, 2, 6, 208, 0, 0, 7, 283, 0, 8, 8, 6, 7, 0, 7, 286, 0, 0, 9, 285, 0, 0, 10, 30, 0, 34, 11, 10, 0, 10, 49, 0, 47, 12, 11778, 0, 3, 1, 0, 0, 5, 49, 13, 2, 9, 11, 10, 12, 43, 9, 6, 8, 3, 0, 7, 13, 2, 6, 208, 0, 0, 7, 283, 0, 8, 8, 6, 7, 0, 7, 157, 0, 0, 9, 285, 0, 0, 10, 30, 0, 34, 11, 10, 0, 10, 49, 0, 47, 12, 11791, 0, 3, 1, 0, 0, 9, 49, 13, 2, 9, 11, 10, 12, 43, 9, 6, 8, 3, 0, 7, 13, 3, 6, 10, 0, 7, 210, 0, 8, 8, 6, 7, 0, 7, 287, 0, 8, 9, 0, 7, 43, 7, 6, 8, 3, 3, 0, 9, 5, 5, 7, 3, 6, 11, 5, 7, 6, 40, 7, 11713, 3, 6, 12, 3, 8, 5, 3, 9, 6, 3, 10, 1, 3, 11, 3, 3, 12, 13, 42, 13, 6, 5, 8, 9, 10, 11, 12, 5, 7, 13, 0, 6, 68, 0, 34, 7, 6, 26, 6, 5, 7, 5, 7, 6, 40, 7, 11759, 0, 6, 288, 0, 8, 8, 0, 6, 43, 6, 0, 8, 0, 0, 6, 289, 0, 8, 8, 0, 6, 43, 6, 0, 8, 0, 5, 7, 6, 0, 6, 0, 0, 45, 6, 4, 1, 3, 2, 0, 45, 2, 0, 2, 0, 0, 45, 2, 4, 1, 3, 2, 0, 45, 2, 0, 2, 0, 0, 45, 2, 4, 1, 3, 2, 0, 45, 2, 0, 2, 0, 0, 45, 2, 4, 5, 0, 6, 180, 0, 8, 7, 5, 6, 0, 6, 30, 0, 34, 8, 6, 43, 6, 5, 7, 5, 0, 1, 2, 3, 8, 45, 6, 0, 6, 0, 0, 45, 6, 4, 3, 0, 4, 290, 0, 0, 5, 13, 0, 8, 6, 4, 5, 2, 5, 291, 0, 42, 7, 5, 1, 0, 0, 5, 292, 0, 43, 8, 4, 6, 2, 7, 5, 0, 4, 13, 0, 8, 5, 8, 4, 2, 4, 291, 0, 0, 6, 193, 0, 8, 7, 1, 6, 3, 6, 0, 0, 9, 293, 0, 43, 10, 1, 7, 2, 6, 9, 42, 6, 4, 1, 10, 43, 4, 8, 5, 1, 6, 45, 4, 0, 4, 0, 0, 45, 4, 4, 1, 47, 3, 11993, 2, 15, 11, 0, 0, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 9, 0, 10, 5, 2, 3, 0, 3, 12, 0, 8, 4, 2, 3, 0, 3, 27, 0, 47, 5, 12428, 2, 6, 1, 0, 1, 2, 9, 4, 3, 5, 45, 2, 0, 3, 0, 0, 45, 3, 4, 3, 40, 0, 12422, 3, 8, 0, 42, 9, 8, 1, 0, 40, 9, 12011, 45, 0, 5, 4, 0, 3, 8, 1, 42, 9, 8, 1, 0, 40, 9, 12290, 5, 8, 1, 41, 8, 12037, 3, 9, 2, 5, 8, 9, 5, 5, 8, 3, 8, 3, 0, 9, 191, 0, 8, 10, 8, 9, 43, 9, 8, 10, 1, 0, 5, 8, 9, 40, 8, 12074, 3, 9, 4, 42, 10, 9, 1, 5, 5, 8, 10, 40, 8, 12151, 0, 8, 294, 0, 8, 9, 5, 8, 0, 8, 18, 0, 8, 10, 0, 8, 0, 8, 68, 0, 43, 11, 0, 10, 1, 8, 0, 8, 193, 0, 8, 10, 11, 8, 2, 8, 21, 0, 0, 12, 194, 0, 0, 13, 42, 0, 44, 14, 8, 2, 12, 13, 0, 8, 23, 0, 43, 12, 11, 10, 2, 14, 8, 43, 8, 5, 9, 1, 12, 5, 9, 8, 39, 12273, 3, 8, 5, 0, 10, 191, 0, 8, 11, 8, 10, 43, 10, 8, 11, 1, 0, 40, 10, 12184, 3, 8, 6, 42, 10, 8, 1, 0, 5, 8, 10, 39, 12270, 3, 10, 0, 42, 11, 10, 1, 5, 40, 11, 12214, 0, 10, 76, 0, 8, 11, 5, 10, 43, 10, 5, 11, 1, 0, 5, 11, 10, 39, 12267, 3, 10, 1, 42, 12, 10, 1, 5, 40, 12, 12252, 3, 10, 7, 42, 12, 10, 1, 5, 0, 10, 76, 0, 8, 13, 12, 10, 43, 10, 12, 13, 1, 0, 5, 12, 10, 39, 12264, 3, 10, 8, 42, 13, 10, 2, 0, 5, 5, 12, 13, 5, 11, 12, 5, 8, 11, 5, 9, 8, 5, 4, 9, 34, 8, 4, 40, 8, 12288, 0, 8, 0, 0, 45, 8, 39, 12317, 3, 8, 9, 42, 9, 8, 1, 0, 40, 9, 12317, 0, 8, 108, 0, 8, 9, 3, 8, 43, 8, 3, 9, 1, 0, 45, 8, 0, 8, 197, 0, 8, 9, 4, 8, 5, 8, 9, 41, 8, 12341, 3, 9, 10, 26, 10, 4, 9, 5, 8, 10, 5, 9, 8, 40, 9, 12357, 48, 8, 1, 4, 5, 4, 8, 5, 9, 4, 0, 8, 29, 0, 0, 9, 29, 0, 8, 10, 4, 9, 9, 3, 8, 10, 0, 8, 30, 0, 5, 6, 8, 0, 8, 29, 0, 8, 9, 3, 8, 5, 7, 9, 22, 8, 6, 7, 40, 8, 12422, 8, 8, 4, 6, 9, 3, 6, 8, 5, 8, 6, 0, 8, 68, 0, 11, 9, 6, 8, 5, 6, 9, 39, 12391, 0, 8, 0, 0, 45, 8, 4, 3, 3, 4, 0, 44, 5, 4, 2, 0, 1, 45, 5, 0, 4, 0, 0, 45, 4, 4, 2, 3, 3, 0, 42, 4, 3, 2, 2, 0, 45, 4, 0, 3, 0, 0, 45, 3, 4, 1, 0, 2, 38, 0, 8, 3, 1, 2, 47, 2, 12497, 2, 7, 0, 0, 43, 4, 1, 3, 1, 2, 45, 4, 0, 2, 0, 0, 45, 2, 4, 3, 0, 4, 253, 0, 8, 5, 1, 4, 40, 5, 12534, 0, 4, 232, 0, 8, 5, 1, 4, 0, 4, 253, 0, 8, 6, 1, 4, 43, 4, 1, 5, 1, 6, 39, 12499, 0, 4, 0, 0, 45, 4, 4, 2, 3, 3, 0, 3, 4, 1, 42, 5, 3, 2, 4, 0, 45, 5, 0, 3, 0, 0, 45, 3, 4, 3, 3, 6, 0, 42, 7, 6, 1, 0, 5, 4, 7, 3, 6, 1, 42, 7, 6, 1, 1, 34, 6, 7, 5, 5, 6, 0, 6, 38, 0, 8, 7, 3, 6, 47, 6, 12628, 2, 9, 5, 0, 0, 2, 0, 3, 1, 4, 1, 5, 1, 1, 43, 8, 3, 7, 1, 6, 45, 8, 0, 6, 0, 0, 45, 6, 4, 3, 3, 4, 0, 42, 5, 4, 1, 1, 5, 4, 5, 40, 4, 12672, 3, 5, 1, 3, 6, 2, 47, 7, 12678, 2, 8, 3, 0, 0, 3, 0, 4, 1, 1, 42, 8, 5, 2, 6, 7, 5, 4, 8, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 40, 4, 12755, 3, 4, 1, 40, 4, 12722, 3, 4, 2, 0, 5, 295, 0, 8, 6, 4, 5, 0, 4, 70, 0, 8, 5, 6, 4, 43, 4, 6, 5, 1, 1, 5, 5, 4, 39, 12750, 3, 4, 2, 0, 6, 295, 0, 8, 7, 4, 6, 0, 4, 177, 0, 8, 6, 7, 4, 43, 4, 7, 6, 1, 1, 5, 5, 4, 5, 4, 5, 39, 12783, 3, 5, 2, 0, 6, 295, 0, 8, 7, 5, 6, 0, 5, 161, 0, 8, 6, 7, 5, 43, 5, 7, 6, 1, 1, 5, 4, 5, 0, 4, 0, 0, 45, 4, 4, 2, 0, 3, 43, 0, 8, 4, 2, 3, 0, 3, 30, 0, 34, 5, 3, 43, 3, 2, 4, 2, 0, 5, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 3, 4, 0, 42, 5, 4, 1, 0, 5, 3, 5, 0, 4, 38, 0, 8, 5, 2, 4, 47, 4, 12869, 2, 9, 3, 0, 0, 1, 0, 2, 1, 3, 43, 6, 2, 5, 1, 4, 45, 6, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 42, 5, 4, 1, 1, 5, 4, 5, 40, 4, 12909, 3, 5, 1, 3, 6, 2, 47, 7, 12915, 2, 7, 1, 0, 1, 1, 42, 8, 5, 2, 6, 7, 5, 4, 8, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 0, 5, 296, 0, 8, 6, 4, 5, 43, 5, 4, 6, 1, 1, 0, 4, 0, 0, 45, 4, 4, 2, 0, 3, 29, 0, 8, 4, 1, 3, 40, 4, 12980, 0, 3, 43, 0, 8, 4, 2, 3, 0, 3, 68, 0, 34, 5, 3, 43, 3, 2, 4, 2, 0, 5, 5, 4, 3, 39, 13006, 0, 3, 46, 0, 8, 5, 2, 3, 0, 3, 112, 0, 0, 6, 23, 0, 43, 7, 2, 5, 2, 3, 6, 5, 4, 7, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 34, 3, 0, 34, 4, 3, 5, 3, 4, 40, 3, 13060, 3, 4, 0, 0, 5, 210, 0, 8, 6, 4, 5, 47, 5, 13068, 1, 8, 2, 0, 0, 1, 1, 0, 43, 7, 4, 6, 2, 2, 5, 5, 3, 7, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 3, 3, 0, 42, 4, 3, 1, 0, 5, 3, 4, 40, 3, 13112, 0, 4, 295, 0, 8, 5, 0, 4, 0, 4, 251, 0, 8, 6, 5, 4, 3, 4, 1, 43, 7, 5, 6, 1, 4, 5, 3, 7, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 3, 3, 0, 42, 4, 3, 1, 0, 40, 4, 13155, 3, 3, 1, 0, 4, 210, 0, 8, 5, 3, 4, 43, 4, 3, 5, 1, 2, 5, 3, 4, 39, 13205, 2, 4, 297, 0, 42, 5, 4, 1, 0, 5, 0, 5, 0, 4, 30, 0, 22, 5, 0, 4, 40, 5, 13195, 0, 4, 29, 0, 8, 5, 2, 4, 11, 4, 0, 5, 5, 5, 4, 39, 13198, 5, 5, 0, 8, 4, 2, 5, 5, 3, 4, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 3, 3, 0, 0, 4, 49, 0, 8, 5, 2, 4, 43, 4, 2, 5, 1, 0, 42, 5, 3, 1, 4, 45, 5, 0, 3, 0, 0, 45, 3, 4, 1, 0, 2, 50, 0, 8, 3, 1, 2, 0, 2, 30, 0, 43, 4, 1, 3, 1, 2, 45, 4, 0, 2, 0, 0, 45, 2, 4, 1, 0, 2, 50, 0, 8, 3, 1, 2, 0, 2, 68, 0, 32, 4, 2, 43, 2, 1, 3, 1, 4, 45, 2, 0, 2, 0, 0, 45, 2, 4, 2, 3, 4, 0, 42, 5, 4, 1, 0, 5, 3, 5, 3, 4, 1, 3, 5, 2, 0, 6, 210, 0, 8, 7, 5, 6, 47, 6, 13360, 2, 7, 1, 0, 1, 3, 43, 8, 5, 7, 2, 2, 6, 42, 5, 4, 1, 8, 45, 5, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 0, 5, 210, 0, 8, 6, 4, 5, 43, 5, 4, 6, 3, 0, 1, 0, 45, 5, 0, 4, 0, 0, 45, 4, 4, 2, 3, 3, 0, 42, 4, 3, 2, 2, 0, 0, 3, 38, 0, 8, 5, 4, 3, 47, 3, 13429, 2, 8, 0, 0, 43, 6, 4, 5, 1, 3, 45, 2, 0, 3, 0, 0, 45, 3, 4, 3, 0, 4, 247, 0, 8, 5, 1, 4, 5, 4, 5, 40, 4, 13470, 0, 5, 247, 0, 8, 6, 1, 5, 0, 5, 232, 0, 8, 7, 6, 5, 43, 5, 6, 7, 1, 1, 5, 4, 5, 0, 4, 0, 0, 45, 4, 4, 2, 3, 4, 0, 42, 5, 4, 1, 0, 40, 5, 13505, 47, 4, 13542, 2, 7, 2, 0, 0, 1, 1, 0, 5, 5, 4, 39, 13517, 47, 4, 13572, 2, 7, 1, 0, 1, 0, 5, 5, 4, 5, 3, 5, 0, 4, 14, 0, 8, 5, 2, 4, 43, 4, 2, 5, 1, 3, 45, 4, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 3, 5, 1, 42, 6, 4, 2, 5, 1, 0, 4, 29, 0, 8, 5, 6, 4, 45, 5, 0, 4, 0, 0, 45, 4, 4, 3, 0, 4, 251, 0, 8, 5, 1, 4, 3, 4, 0, 43, 6, 1, 5, 1, 4, 45, 6, 0, 4, 0, 0, 45, 4, 4, 2, 3, 4, 0, 42, 5, 4, 1, 0, 5, 3, 5, 0, 4, 14, 0, 8, 5, 2, 4, 47, 4, 13649, 2, 8, 4, 0, 0, 1, 1, 0, 0, 2, 1, 3, 43, 6, 2, 5, 1, 4, 45, 6, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 3, 5, 1, 42, 6, 4, 1, 5, 34, 4, 6, 5, 5, 4, 41, 5, 13682, 3, 4, 2, 42, 6, 4, 1, 1, 5, 5, 6, 5, 4, 5, 40, 4, 13713, 3, 5, 3, 0, 6, 210, 0, 8, 7, 5, 6, 43, 6, 5, 7, 3, 1, 0, 1, 34, 5, 6, 5, 4, 5, 45, 4, 0, 4, 0, 0, 45, 4, 4, 2, 3, 4, 0, 42, 5, 4, 1, 0, 5, 3, 5, 3, 4, 1, 0, 5, 210, 0, 8, 6, 4, 5, 47, 5, 13769, 2, 7, 1, 0, 1, 3, 43, 7, 4, 6, 2, 2, 5, 45, 7, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 0, 5, 210, 0, 8, 6, 4, 5, 43, 5, 4, 6, 3, 0, 1, 0, 45, 5, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 3, 5, 1, 0, 6, 49, 0, 8, 7, 3, 6, 43, 6, 3, 7, 0, 0, 7, 13, 0, 8, 8, 6, 7, 3, 7, 0, 42, 9, 7, 2, 0, 1, 0, 7, 49, 0, 8, 10, 9, 7, 43, 7, 9, 10, 0, 43, 9, 6, 8, 1, 7, 42, 6, 5, 1, 9, 42, 5, 4, 1, 6, 45, 5, 0, 4, 0, 0, 45, 4, 4, 2, 3, 3, 0, 3, 4, 1, 3, 5, 2, 3, 6, 3, 47, 7, 13924, 1, 5, 0, 0, 42, 8, 6, 2, 2, 7, 42, 6, 5, 1, 8, 42, 5, 4, 1, 6, 42, 4, 3, 2, 5, 0, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 0, 3, 71, 0, 8, 4, 0, 3, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 3, 3, 0, 3, 4, 1, 3, 5, 2, 3, 6, 3, 0, 7, 247, 0, 42, 8, 6, 2, 2, 7, 42, 6, 5, 1, 8, 42, 5, 4, 1, 6, 42, 4, 3, 2, 5, 0, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 40, 0, 14016, 3, 5, 0, 42, 6, 5, 1, 0, 0, 5, 30, 0, 8, 7, 6, 5, 5, 5, 7, 39, 14027, 0, 6, 30, 0, 8, 7, 2, 6, 5, 5, 7, 5, 3, 5, 40, 0, 14038, 5, 5, 2, 39, 14075, 3, 6, 0, 42, 7, 6, 1, 3, 0, 6, 72, 0, 8, 8, 7, 6, 43, 6, 7, 8, 0, 0, 7, 71, 0, 8, 8, 6, 7, 43, 7, 6, 8, 0, 5, 5, 7, 5, 4, 5, 3, 5, 1, 0, 6, 210, 0, 8, 7, 5, 6, 43, 6, 5, 7, 2, 4, 3, 45, 6, 0, 5, 0, 0, 45, 5, 4, 2, 0, 5, 14, 0, 8, 6, 2, 5, 43, 5, 2, 6, 1, 0, 5, 3, 5, 0, 5, 29, 0, 8, 6, 3, 5, 40, 6, 14136, 45, 3, 0, 5, 72, 0, 8, 6, 2, 5, 43, 5, 2, 6, 0, 5, 4, 5, 0, 5, 29, 0, 8, 6, 4, 5, 40, 6, 14182, 0, 5, 74, 0, 8, 6, 4, 5, 43, 5, 4, 6, 1, 0, 5, 6, 5, 39, 14185, 5, 6, 3, 45, 6, 0, 5, 0, 0, 45, 5, 4, 2, 3, 3, 0, 3, 4, 1, 3, 5, 2, 3, 6, 3, 47, 7, 14246, 1, 6, 1, 0, 0, 1, 42, 8, 6, 2, 2, 7, 42, 6, 5, 1, 8, 42, 5, 4, 1, 6, 42, 4, 3, 2, 5, 0, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 3, 3, 0, 42, 4, 3, 1, 0, 0, 3, 72, 0, 8, 5, 4, 3, 43, 3, 4, 5, 0, 0, 4, 71, 0, 8, 5, 3, 4, 43, 4, 3, 5, 0, 0, 3, 64, 0, 8, 5, 4, 3, 43, 3, 4, 5, 1, 0, 45, 3, 0, 3, 0, 0, 45, 3, 4, 2, 3, 3, 0, 3, 4, 1, 3, 5, 2, 47, 6, 14350, 1, 6, 2, 0, 0, 3, 1, 0, 42, 7, 5, 2, 2, 6, 42, 5, 4, 1, 7, 42, 4, 3, 1, 5, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 3, 3, 0, 3, 4, 1, 42, 5, 3, 2, 4, 0, 45, 5, 0, 3, 0, 0, 45, 3, 4, 1, 3, 2, 0, 0, 3, 68, 0, 34, 4, 3, 0, 3, 68, 0, 34, 5, 3, 0, 3, 68, 0, 34, 6, 3, 0, 3, 30, 0, 34, 7, 3, 0, 3, 30, 0, 34, 8, 3, 42, 3, 2, 7, 0, 1, 4, 5, 6, 7, 8, 45, 3, 0, 2, 0, 0, 45, 2, 4, 1, 3, 2, 0, 0, 3, 68, 0, 34, 4, 3, 0, 3, 68, 0, 34, 5, 3, 0, 3, 30, 0, 34, 6, 3, 42, 3, 2, 5, 0, 1, 4, 5, 6, 45, 3, 0, 2, 0, 0, 45, 2, 4, 2, 3, 3, 0, 0, 4, 30, 0, 34, 5, 4, 0, 4, 68, 0, 34, 6, 4, 0, 4, 30, 0, 34, 7, 4, 42, 4, 3, 5, 1, 2, 5, 6, 7, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 0, 3, 38, 0, 8, 4, 2, 3, 47, 3, 14552, 2, 10, 2, 0, 0, 0, 1, 0, 43, 5, 2, 4, 1, 3, 45, 5, 0, 3, 0, 0, 45, 3, 4, 3, 3, 6, 0, 42, 7, 6, 1, 1, 5, 4, 7, 0, 6, 96, 0, 8, 7, 4, 6, 43, 6, 4, 7, 0, 5, 5, 6, 0, 6, 29, 0, 8, 7, 5, 6, 40, 7, 14614, 0, 6, 90, 0, 8, 7, 5, 6, 3, 6, 1, 43, 8, 5, 7, 1, 6, 5, 6, 8, 39, 14634, 0, 7, 85, 0, 8, 8, 4, 7, 3, 7, 1, 43, 9, 4, 8, 1, 7, 5, 6, 9, 0, 6, 0, 0, 45, 6, 4, 1, 3, 2, 0, 0, 3, 68, 0, 34, 4, 3, 0, 3, 30, 0, 34, 5, 3, 42, 3, 2, 4, 0, 1, 4, 5, 45, 3, 0, 2, 0, 0, 45, 2, 4, 2, 3, 5, 0, 42, 6, 5, 1, 0, 5, 3, 6, 0, 5, 30, 0, 8, 6, 3, 5, 5, 4, 6, 0, 5, 71, 0, 8, 6, 4, 5, 0, 5, 29, 0, 8, 7, 6, 5, 40, 7, 14731, 0, 5, 298, 0, 8, 6, 4, 5, 5, 4, 6, 39, 14699, 0, 5, 51, 0, 8, 6, 2, 5, 43, 5, 2, 6, 0, 0, 6, 89, 0, 8, 7, 5, 6, 43, 6, 5, 7, 1, 3, 0, 5, 87, 0, 8, 6, 2, 5, 43, 5, 2, 6, 1, 4, 45, 5, 0, 5, 0, 0, 45, 5, 4, 2, 0, 3, 38, 0, 8, 4, 2, 3, 47, 3, 14815, 2, 10, 2, 0, 0, 0, 1, 0, 43, 5, 2, 4, 1, 3, 45, 5, 0, 3, 0, 0, 45, 3, 4, 3, 3, 5, 0, 3, 6, 1, 42, 7, 5, 1, 6, 0, 5, 30, 0, 8, 6, 7, 5, 5, 4, 6, 3, 5, 0, 42, 6, 5, 1, 1, 0, 5, 90, 0, 8, 7, 6, 5, 40, 0, 14884, 0, 5, 259, 0, 8, 8, 4, 5, 0, 5, 30, 0, 34, 9, 5, 43, 5, 4, 8, 1, 9, 5, 8, 5, 39, 14887, 5, 8, 4, 43, 5, 6, 7, 1, 8, 0, 5, 0, 0, 45, 5, 4, 2, 3, 3, 0, 0, 4, 30, 0, 34, 5, 4, 0, 4, 68, 0, 34, 6, 4, 0, 4, 68, 0, 34, 7, 4, 0, 4, 68, 0, 34, 8, 4, 0, 4, 68, 0, 34, 9, 4, 0, 4, 30, 0, 34, 10, 4, 42, 4, 3, 8, 1, 2, 5, 6, 7, 8, 9, 10, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 3, 3, 0, 0, 4, 30, 0, 34, 5, 4, 0, 4, 30, 0, 34, 6, 4, 42, 4, 3, 4, 1, 2, 5, 6, 45, 4, 0, 3, 0, 0, 45, 3, 4, 1, 3, 2, 0, 0, 3, 68, 0, 34, 4, 3, 0, 3, 30, 0, 34, 5, 3, 0, 3, 30, 0, 34, 6, 3, 0, 3, 30, 0, 34, 7, 3, 0, 3, 30, 0, 34, 8, 3, 42, 3, 2, 7, 0, 1, 4, 5, 6, 7, 8, 45, 3, 0, 2, 0, 0, 45, 2, 4, 2, 3, 3, 0, 0, 4, 30, 0, 34, 5, 4, 0, 4, 30, 0, 34, 6, 4, 0, 4, 30, 0, 34, 7, 4, 0, 4, 68, 0, 34, 8, 4, 0, 4, 68, 0, 34, 9, 4, 0, 4, 30, 0, 34, 10, 4, 42, 4, 3, 8, 1, 2, 5, 6, 7, 8, 9, 10, 45, 4, 0, 3, 0, 0, 45, 3, 4, 1, 3, 2, 0, 3, 3, 1, 3, 4, 2, 47, 5, 15169, 1, 7, 0, 0, 42, 6, 4, 2, 1, 5, 42, 4, 3, 1, 6, 42, 3, 2, 1, 4, 45, 3, 0, 2, 0, 0, 45, 2, 4, 2, 0, 3, 299, 0, 8, 4, 0, 3, 0, 3, 300, 0, 26, 5, 4, 3, 40, 5, 15207, 0, 3, 301, 0, 8, 4, 0, 3, 48, 3, 1, 4, 5, 4, 3, 39, 15261, 0, 3, 299, 0, 8, 5, 0, 3, 0, 3, 302, 0, 26, 6, 5, 3, 40, 6, 15247, 0, 3, 303, 0, 8, 5, 0, 3, 0, 3, 240, 0, 8, 6, 5, 3, 5, 3, 6, 39, 15258, 0, 5, 240, 0, 8, 6, 0, 5, 5, 3, 6, 5, 4, 3, 45, 4, 0, 3, 0, 0, 45, 3, 4, 4, 3, 5, 0, 3, 6, 1, 3, 7, 2, 3, 8, 3, 0, 9, 304, 0, 42, 10, 8, 4, 4, 9, 1, 2, 42, 8, 7, 1, 10, 42, 7, 6, 1, 8, 42, 6, 5, 2, 7, 0, 45, 6, 0, 5, 0, 0, 45, 5, 4, 2, 0, 3, 97, 0, 8, 4, 2, 3, 0, 3, 30, 0, 34, 5, 3, 43, 3, 2, 4, 2, 0, 5, 45, 3, 0, 3, 0, 0, 45, 3, 4, 3, 0, 4, 97, 0, 8, 5, 3, 4, 0, 4, 30, 0, 34, 6, 4, 43, 4, 3, 5, 3, 1, 6, 0, 45, 4, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 3, 5, 1, 3, 6, 2, 3, 7, 3, 0, 8, 305, 0, 0, 9, 30, 0, 34, 10, 9, 42, 9, 7, 4, 3, 8, 10, 1, 42, 7, 6, 1, 9, 42, 6, 5, 1, 7, 42, 5, 4, 2, 6, 0, 45, 5, 0, 4, 0, 0, 45, 4, 4, 3, 0, 4, 100, 0, 8, 5, 3, 4, 43, 4, 3, 5, 2, 1, 0, 45, 4, 0, 4, 0, 0, 45, 4, 4, 4, 3, 5, 0, 3, 6, 1, 3, 7, 2, 3, 8, 3, 0, 9, 306, 0, 42, 10, 8, 4, 4, 9, 1, 2, 42, 8, 7, 1, 10, 42, 7, 6, 1, 8, 42, 6, 5, 2, 7, 0, 45, 6, 0, 5, 0, 0, 45, 5, 4, 2, 0, 3, 102, 0, 8, 4, 2, 3, 0, 3, 30, 0, 34, 5, 3, 43, 3, 2, 4, 2, 0, 5, 45, 3, 0, 3, 0, 0, 45, 3, 4, 3, 0, 4, 102, 0, 8, 5, 3, 4, 0, 4, 30, 0, 34, 6, 4, 43, 4, 3, 5, 3, 1, 6, 0, 45, 4, 0, 4, 0, 0, 45, 4, 4, 2, 3, 3, 0, 3, 4, 1, 0, 5, 241, 0, 8, 6, 4, 5, 48, 5, 0, 3, 7, 2, 0, 8, 210, 0, 8, 9, 7, 8, 47, 8, 15647, 2, 7, 1, 0, 1, 0, 43, 10, 7, 9, 2, 2, 8, 43, 7, 4, 6, 2, 5, 10, 42, 4, 3, 1, 7, 45, 4, 0, 3, 0, 0, 45, 3, 4, 3, 3, 4, 0, 0, 5, 210, 0, 8, 6, 4, 5, 43, 5, 4, 6, 3, 0, 1, 0, 45, 5, 0, 4, 0, 0, 45, 4, 4, 1, 0, 2, 16, 0, 8, 3, 1, 2, 47, 2, 15707, 2, 7, 0, 0, 43, 4, 1, 3, 1, 2, 45, 4, 0, 2, 0, 0, 45, 2, 4, 3, 0, 4, 259, 0, 8, 5, 1, 4, 0, 4, 30, 0, 34, 6, 4, 43, 4, 1, 5, 1, 6, 45, 4, 0, 4, 0, 0, 45, 4, 4, 1, 0, 2, 16, 0, 8, 3, 1, 2, 47, 2, 15773, 2, 9, 2, 0, 0, 0, 0, 1, 43, 4, 1, 3, 1, 2, 45, 4, 0, 2, 0, 0, 45, 2, 4, 3, 0, 5, 106, 0, 8, 6, 1, 5, 5, 4, 6, 5, 5, 4, 40, 5, 15816, 3, 6, 0, 0, 7, 111, 0, 42, 8, 6, 2, 4, 7, 0, 6, 307, 0, 26, 7, 8, 6, 5, 5, 7, 40, 5, 15832, 0, 5, 106, 0, 8, 6, 4, 5, 5, 4, 6, 39, 15786, 5, 5, 4, 41, 5, 15844, 3, 6, 1, 5, 5, 6, 45, 5, 0, 5, 0, 0, 45, 5, 4, 3, 3, 4, 0, 3, 5, 1, 0, 6, 210, 0, 8, 7, 5, 6, 43, 6, 5, 7, 3, 3, 0, 1, 42, 5, 4, 1, 6, 45, 5, 0, 4, 0, 0, 45, 4, 4, 2, 47, 4, 15969, 0, 7, 2, 0, 1, 0, 0, 0, 5, 3, 4, 3, 4, 1, 0, 5, 308, 0, 8, 6, 4, 5, 0, 4, 309, 0, 27, 5, 6, 4, 40, 5, 15936, 42, 4, 3, 0, 5, 5, 4, 39, 15961, 3, 4, 1, 0, 6, 275, 0, 8, 7, 4, 6, 0, 6, 310, 0, 43, 8, 4, 7, 2, 6, 3, 5, 5, 8, 45, 2, 0, 4, 0, 0, 45, 4, 4, 1, 2, 2, 311, 0, 3, 3, 0, 0, 4, 30, 0, 3, 5, 1, 42, 6, 2, 3, 3, 4, 5, 45, 6, 0, 2, 0, 0, 45, 2, 4, 1, 0, 2, 72, 0, 8, 3, 1, 2, 43, 2, 1, 3, 0, 0, 3, 38, 0, 8, 4, 2, 3, 47, 3, 16046, 2, 8, 1, 0, 0, 0, 43, 5, 2, 4, 1, 3, 45, 1, 0, 2, 0, 0, 45, 2, 4, 3, 0, 5, 299, 0, 8, 6, 1, 5, 0, 5, 312, 0, 27, 7, 6, 5, 40, 7, 16105, 3, 5, 0, 42, 6, 5, 1, 1, 5, 4, 6, 0, 5, 178, 0, 8, 6, 4, 5, 0, 5, 71, 0, 8, 7, 4, 5, 43, 5, 4, 7, 0, 43, 7, 4, 6, 1, 5, 0, 5, 0, 0, 45, 5, 4, 1, 0, 4, 30, 0, 8, 5, 1, 4, 5, 2, 5, 40, 2, 16206, 0, 4, 313, 0, 8, 5, 2, 4, 43, 4, 2, 5, 0, 5, 3, 4, 0, 4, 314, 0, 0, 5, 314, 0, 8, 6, 3, 5, 3, 5, 0, 0, 7, 315, 0, 8, 8, 5, 7, 11, 5, 6, 8, 0, 6, 316, 0, 0, 7, 316, 0, 8, 8, 3, 7, 3, 7, 0, 0, 9, 317, 0, 8, 10, 7, 9, 11, 7, 8, 10, 49, 8, 2, 4, 5, 6, 7, 45, 8, 0, 4, 0, 0, 45, 4, 4, 1, 0, 8, 30, 0, 8, 9, 1, 8, 5, 2, 9, 40, 2, 16613, 3, 8, 0, 0, 9, 111, 0, 42, 10, 8, 2, 2, 9, 0, 8, 318, 0, 26, 9, 10, 8, 5, 3, 9, 40, 3, 16273, 0, 8, 313, 0, 8, 9, 2, 8, 43, 8, 2, 9, 0, 5, 9, 8, 39, 16289, 0, 8, 110, 0, 8, 10, 1, 8, 43, 8, 1, 10, 0, 5, 9, 8, 5, 4, 9, 34, 8, 3, 40, 8, 16546, 0, 8, 258, 0, 8, 9, 2, 8, 5, 5, 9, 0, 8, 106, 0, 8, 9, 2, 8, 5, 8, 9, 41, 8, 16334, 0, 9, 3, 0, 8, 10, 5, 9, 5, 8, 10, 5, 6, 8, 0, 8, 230, 0, 8, 9, 5, 8, 26, 8, 6, 9, 5, 9, 8, 41, 9, 16370, 0, 8, 3, 0, 8, 10, 5, 8, 26, 8, 6, 10, 5, 9, 8, 5, 8, 9, 40, 8, 16400, 3, 9, 0, 0, 10, 111, 0, 42, 11, 9, 2, 6, 10, 0, 9, 307, 0, 26, 10, 11, 9, 5, 8, 10, 40, 8, 16416, 0, 8, 247, 0, 8, 9, 6, 8, 5, 6, 9, 39, 16337, 27, 8, 6, 2, 5, 9, 8, 40, 9, 16437, 3, 8, 1, 42, 10, 8, 1, 6, 5, 9, 10, 40, 9, 16546, 3, 8, 2, 42, 9, 8, 1, 6, 0, 8, 110, 0, 8, 10, 9, 8, 43, 8, 9, 10, 0, 5, 7, 8, 0, 8, 314, 0, 8, 9, 4, 8, 0, 10, 314, 0, 8, 11, 7, 10, 3, 10, 3, 0, 12, 319, 0, 42, 13, 10, 2, 6, 12, 11, 10, 11, 13, 12, 11, 9, 10, 9, 4, 8, 11, 0, 8, 316, 0, 8, 9, 4, 8, 0, 10, 316, 0, 8, 11, 7, 10, 3, 10, 3, 0, 12, 320, 0, 42, 13, 10, 2, 6, 12, 11, 10, 11, 13, 12, 11, 9, 10, 9, 4, 8, 11, 0, 8, 314, 0, 0, 9, 314, 0, 8, 10, 4, 9, 3, 9, 3, 0, 11, 321, 0, 42, 12, 9, 2, 2, 11, 12, 9, 10, 12, 0, 10, 316, 0, 0, 11, 316, 0, 8, 12, 4, 11, 3, 11, 3, 0, 13, 322, 0, 42, 14, 11, 2, 2, 13, 12, 11, 12, 14, 49, 12, 2, 8, 9, 10, 11, 45, 12, 0, 8, 0, 0, 45, 8, 4, 3, 40, 0, 16770, 3, 6, 0, 42, 7, 6, 1, 0, 40, 7, 16737, 3, 6, 1, 8, 7, 6, 0, 5, 6, 7, 41, 6, 16651, 5, 6, 0, 5, 0, 6, 0, 6, 29, 0, 8, 7, 2, 6, 0, 6, 215, 0, 22, 8, 7, 6, 40, 8, 16707, 0, 6, 30, 0, 8, 7, 3, 6, 5, 6, 7, 40, 6, 16702, 0, 7, 30, 0, 8, 8, 3, 7, 8, 7, 8, 0, 5, 6, 7, 5, 7, 6, 39, 16735, 0, 6, 38, 0, 8, 8, 3, 6, 47, 6, 16776, 2, 6, 2, 0, 1, 0, 1, 1, 43, 9, 3, 8, 1, 6, 5, 7, 9, 45, 7, 52, 4, 0, 53, 6, 4, 16768, 5, 5, 6, 0, 6, 130, 0, 8, 7, 3, 6, 8, 6, 0, 5, 43, 8, 3, 7, 2, 5, 6, 39, 16740, 45, 3, 0, 6, 0, 0, 45, 6, 4, 3, 3, 4, 0, 3, 5, 1, 9, 1, 4, 5, 0, 4, 0, 0, 45, 4, 4, 2, 0, 3, 38, 0, 8, 4, 2, 3, 47, 3, 16829, 2, 7, 2, 0, 0, 0, 1, 0, 43, 5, 2, 4, 1, 3, 45, 5, 0, 3, 0, 0, 45, 3, 4, 3, 3, 4, 0, 3, 5, 1, 8, 6, 4, 5, 5, 4, 6, 41, 4, 16853, 3, 5, 1, 5, 4, 5, 10, 5, 1, 4, 0, 4, 0, 0, 45, 4, 4, 3, 3, 4, 0, 0, 5, 158, 0, 0, 6, 159, 0, 48, 7, 2, 5, 6, 47, 5, 16912, 2, 9, 6, 0, 1, 1, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 42, 6, 4, 2, 7, 5, 0, 4, 0, 0, 45, 4, 4, 3, 0, 5, 23, 0, 0, 6, 13, 0, 8, 7, 5, 6, 3, 6, 0, 40, 6, 16941, 0, 6, 323, 0, 5, 8, 6, 39, 16948, 0, 6, 324, 0, 5, 8, 6, 43, 6, 5, 7, 1, 8, 0, 5, 13, 0, 8, 7, 6, 5, 43, 5, 6, 7, 1, 1, 5, 4, 5, 3, 5, 1, 47, 6, 17005, 1, 13, 7, 0, 0, 2, 0, 0, 1, 1, 0, 3, 0, 4, 0, 5, 1, 0, 9, 5, 4, 6, 0, 5, 0, 0, 45, 5, 4, 2, 0, 3, 30, 0, 8, 4, 2, 3, 40, 4, 17408, 3, 3, 0, 0, 4, 30, 0, 8, 5, 2, 4, 42, 4, 3, 1, 5, 40, 4, 17138, 3, 3, 1, 40, 3, 17081, 0, 3, 30, 0, 8, 4, 2, 3, 0, 3, 324, 0, 0, 5, 13, 0, 8, 6, 3, 5, 3, 5, 2, 43, 7, 3, 6, 1, 5, 8, 3, 4, 7, 5, 4, 3, 39, 17133, 0, 3, 30, 0, 8, 5, 2, 3, 0, 3, 1, 0, 8, 6, 5, 3, 0, 3, 3, 0, 8, 5, 6, 3, 0, 3, 273, 0, 0, 6, 13, 0, 8, 7, 3, 6, 3, 6, 2, 43, 8, 3, 7, 1, 6, 8, 3, 5, 8, 5, 4, 3, 5, 3, 4, 39, 17406, 3, 4, 3, 0, 5, 30, 0, 8, 6, 2, 5, 42, 5, 4, 1, 6, 40, 5, 17182, 3, 4, 4, 0, 5, 30, 0, 8, 6, 2, 5, 3, 5, 2, 42, 7, 4, 2, 6, 5, 5, 4, 7, 39, 17403, 0, 5, 30, 0, 8, 6, 2, 5, 0, 5, 23, 0, 0, 7, 13, 0, 8, 8, 5, 7, 3, 7, 1, 40, 7, 17217, 0, 7, 110, 0, 5, 9, 7, 39, 17224, 0, 7, 273, 0, 5, 9, 7, 43, 7, 5, 8, 1, 9, 0, 5, 13, 0, 8, 8, 7, 5, 3, 5, 2, 43, 9, 7, 8, 1, 5, 8, 5, 6, 9, 5, 6, 0, 40, 6, 17263, 3, 7, 1, 5, 6, 7, 40, 6, 17389, 3, 6, 5, 0, 7, 30, 0, 8, 8, 2, 7, 0, 7, 325, 0, 0, 9, 13, 0, 8, 10, 7, 9, 3, 9, 6, 40, 9, 17304, 0, 9, 226, 0, 5, 11, 9, 39, 17311, 0, 9, 225, 0, 5, 11, 9, 43, 9, 7, 10, 1, 11, 42, 7, 6, 2, 8, 9, 3, 6, 5, 0, 8, 30, 0, 8, 9, 2, 8, 0, 8, 325, 0, 0, 10, 13, 0, 8, 11, 8, 10, 3, 10, 6, 40, 10, 17361, 0, 10, 229, 0, 5, 12, 10, 39, 17368, 0, 10, 228, 0, 5, 12, 10, 43, 10, 8, 11, 1, 12, 42, 8, 6, 2, 9, 10, 11, 6, 7, 8, 5, 7, 6, 39, 17396, 0, 6, 30, 0, 5, 7, 6, 11, 6, 5, 7, 5, 4, 6, 5, 3, 4, 45, 3, 0, 3, 0, 0, 45, 3, 4, 3, 0, 5, 326, 0, 8, 6, 1, 5, 43, 5, 1, 6, 0, 5, 4, 5, 3, 5, 0, 47, 6, 17474, 1, 11, 11, 0, 0, 1, 0, 2, 1, 1, 0, 3, 0, 4, 1, 4, 0, 5, 1, 0, 0, 6, 0, 7, 0, 8, 9, 5, 4, 6, 0, 5, 0, 0, 45, 5, 4, 2, 0, 4, 30, 0, 8, 5, 2, 4, 34, 4, 5, 40, 4, 17518, 3, 4, 0, 42, 5, 4, 1, 0, 40, 5, 17513, 0, 4, 30, 0, 37, 5, 4, 5, 4, 5, 39, 17516, 5, 4, 2, 45, 4, 0, 4, 29, 0, 8, 5, 1, 4, 34, 4, 5, 40, 4, 17712, 3, 4, 1, 0, 5, 30, 0, 8, 6, 2, 5, 42, 5, 4, 1, 6, 40, 5, 17605, 0, 4, 30, 0, 8, 5, 2, 4, 0, 4, 1, 0, 8, 6, 5, 4, 0, 4, 3, 0, 8, 5, 6, 4, 0, 4, 273, 0, 0, 6, 13, 0, 8, 7, 4, 6, 3, 6, 2, 43, 8, 4, 7, 1, 6, 8, 4, 5, 8, 5, 5, 4, 39, 17710, 3, 4, 3, 0, 6, 30, 0, 8, 7, 2, 6, 42, 6, 4, 1, 7, 40, 6, 17649, 3, 4, 4, 0, 6, 30, 0, 8, 7, 2, 6, 3, 6, 2, 42, 8, 4, 2, 7, 6, 5, 4, 8, 39, 17707, 0, 6, 30, 0, 8, 7, 2, 6, 0, 6, 313, 0, 8, 8, 7, 6, 43, 6, 7, 8, 0, 3, 7, 5, 8, 8, 6, 7, 3, 6, 6, 0, 7, 30, 0, 8, 9, 2, 7, 3, 7, 7, 34, 10, 7, 42, 7, 6, 2, 9, 10, 12, 6, 8, 7, 5, 4, 6, 5, 5, 4, 45, 5, 2, 4, 222, 0, 0, 5, 223, 0, 42, 6, 4, 2, 0, 5, 5, 3, 6, 0, 4, 38, 0, 8, 5, 2, 4, 47, 4, 17772, 2, 13, 7, 0, 0, 8, 0, 9, 0, 5, 0, 10, 1, 3, 0, 6, 0, 7, 43, 6, 2, 5, 1, 4, 45, 6, 0, 4, 0, 0, 45, 4, 4, 3, 3, 5, 0, 42, 6, 5, 1, 1, 40, 6, 17873, 3, 5, 1, 0, 6, 327, 0, 42, 7, 5, 2, 1, 6, 5, 4, 7, 0, 5, 133, 0, 8, 6, 1, 5, 3, 5, 2, 3, 7, 3, 3, 8, 2, 3, 9, 4, 0, 10, 328, 0, 26, 11, 4, 10, 40, 11, 17852, 3, 10, 5, 3, 11, 6, 34, 12, 11, 42, 11, 10, 2, 1, 12, 5, 10, 11, 39, 17859, 0, 11, 30, 0, 5, 10, 11, 11, 11, 9, 10, 42, 9, 7, 2, 8, 11, 9, 6, 5, 9, 0, 5, 0, 0, 45, 5, 4, 2, 0, 3, 38, 0, 8, 4, 2, 3, 47, 3, 17924, 2, 12, 7, 0, 0, 0, 0, 1, 0, 2, 1, 0, 0, 3, 0, 4, 0, 5, 43, 5, 2, 4, 1, 3, 45, 5, 0, 3, 0, 0, 45, 3, 4, 3, 3, 6, 0, 42, 7, 6, 1, 1, 40, 7, 18121, 3, 6, 1, 42, 7, 6, 1, 1, 5, 4, 7, 3, 6, 2, 3, 7, 3, 42, 8, 6, 1, 7, 40, 8, 17967, 5, 6, 4, 39, 17973, 3, 7, 3, 5, 6, 7, 5, 5, 6, 40, 5, 18069, 0, 6, 133, 0, 8, 7, 1, 6, 0, 6, 231, 0, 3, 8, 4, 8, 9, 1, 8, 5, 8, 9, 41, 8, 18011, 0, 9, 23, 0, 5, 8, 9, 9, 7, 6, 8, 3, 6, 1, 42, 7, 6, 1, 1, 5, 6, 7, 40, 6, 18064, 0, 7, 133, 0, 8, 8, 1, 7, 0, 7, 231, 0, 3, 9, 5, 0, 10, 299, 0, 8, 11, 1, 10, 42, 10, 9, 1, 11, 9, 8, 7, 10, 5, 6, 10, 5, 7, 6, 39, 18121, 5, 6, 4, 41, 6, 18118, 3, 8, 4, 3, 9, 6, 0, 10, 231, 0, 42, 11, 9, 2, 1, 10, 9, 1, 8, 11, 0, 8, 133, 0, 8, 9, 1, 8, 0, 8, 231, 0, 0, 10, 233, 0, 9, 9, 8, 10, 5, 6, 10, 5, 7, 6, 0, 6, 0, 0, 45, 6, 4, 1, 0, 2, 161, 0, 8, 3, 1, 2, 0, 2, 68, 0, 34, 4, 2, 43, 2, 1, 3, 1, 4, 45, 2, 0, 2, 0, 0, 45, 2, 4, 1, 0, 2, 161, 0, 8, 3, 1, 2, 0, 2, 30, 0, 34, 4, 2, 43, 2, 1, 3, 1, 4, 45, 2, 0, 2, 0, 0, 45, 2, 4, 3, 3, 10, 0, 42, 11, 10, 1, 0, 40, 11, 18376, 3, 10, 1, 42, 11, 10, 1, 0, 5, 4, 11, 0, 10, 30, 0, 8, 11, 4, 10, 5, 5, 11, 0, 10, 68, 0, 8, 11, 4, 10, 5, 6, 11, 3, 10, 2, 42, 11, 10, 1, 5, 5, 7, 11, 34, 10, 7, 40, 10, 18254, 45, 3, 3, 10, 3, 0, 11, 191, 0, 8, 12, 10, 11, 43, 11, 10, 12, 1, 7, 40, 11, 18283, 0, 10, 329, 0, 5, 11, 10, 39, 18290, 0, 10, 330, 0, 5, 11, 10, 5, 8, 11, 3, 10, 4, 0, 11, 331, 0, 8, 12, 10, 11, 43, 11, 10, 12, 1, 8, 5, 0, 11, 0, 10, 332, 0, 8, 11, 0, 10, 0, 10, 30, 0, 34, 12, 10, 0, 10, 30, 0, 34, 13, 10, 43, 10, 0, 11, 3, 7, 12, 13, 0, 10, 280, 0, 0, 11, 218, 0, 8, 12, 6, 11, 3, 11, 5, 43, 13, 6, 12, 1, 11, 9, 0, 10, 13, 0, 10, 281, 0, 9, 0, 10, 5, 0, 10, 287, 0, 9, 0, 10, 1, 0, 10, 281, 0, 8, 11, 0, 10, 3, 10, 6, 30, 12, 11, 10, 5, 9, 12, 0, 10, 38, 0, 8, 11, 3, 10, 47, 10, 18437, 2, 10, 3, 0, 1, 9, 0, 7, 1, 0, 43, 12, 3, 11, 1, 10, 45, 12, 0, 10, 0, 0, 45, 10, 4, 3, 3, 4, 0, 5, 5, 4, 40, 5, 18474, 3, 4, 1, 3, 6, 2, 0, 7, 281, 0, 8, 8, 6, 7, 8, 6, 1, 8, 42, 7, 4, 1, 6, 5, 5, 7, 5, 4, 5, 40, 4, 18583, 0, 5, 278, 0, 0, 6, 13, 0, 8, 7, 5, 6, 3, 6, 2, 0, 8, 80, 0, 8, 9, 6, 8, 43, 6, 5, 7, 1, 9, 0, 5, 30, 0, 34, 7, 5, 9, 1, 6, 7, 3, 5, 2, 0, 6, 281, 0, 8, 7, 5, 6, 8, 5, 1, 7, 43, 6, 1, 5, 0, 0, 5, 278, 0, 0, 6, 13, 0, 8, 7, 5, 6, 3, 6, 2, 0, 8, 80, 0, 8, 9, 6, 8, 43, 6, 5, 7, 1, 9, 0, 5, 68, 0, 34, 7, 5, 9, 1, 6, 7, 5, 4, 7, 0, 4, 333, 0, 8, 5, 1, 4, 3, 4, 2, 43, 6, 1, 5, 1, 4, 0, 4, 0, 0, 45, 4, 4, 4, 5, 5, 4, 3, 8, 0, 42, 9, 8, 1, 0, 40, 9, 18653, 0, 8, 38, 0, 8, 9, 4, 8, 47, 8, 18774, 2, 7, 4, 0, 0, 1, 0, 2, 0, 3, 0, 4, 43, 10, 4, 9, 1, 8, 39, 18766, 3, 8, 5, 42, 9, 8, 1, 0, 40, 9, 18735, 3, 8, 6, 42, 9, 8, 1, 1, 5, 8, 9, 40, 8, 18691, 5, 2, 1, 0, 9, 23, 0, 5, 1, 9, 5, 8, 1, 3, 8, 7, 3, 9, 8, 42, 10, 9, 1, 0, 47, 9, 18850, 2, 12, 9, 0, 0, 9, 0, 10, 1, 5, 0, 1, 0, 2, 0, 3, 0, 4, 1, 1, 1, 2, 42, 11, 8, 2, 10, 9, 39, 18766, 52, 6, 0, 53, 8, 6, 18766, 5, 7, 8, 0, 8, 176, 0, 8, 9, 4, 8, 8, 8, 0, 7, 43, 10, 4, 9, 2, 7, 8, 39, 18738, 45, 4, 0, 8, 0, 0, 45, 8, 4, 3, 3, 4, 0, 42, 5, 4, 1, 1, 34, 4, 5, 5, 5, 4, 40, 5, 18807, 3, 4, 1, 42, 6, 4, 1, 1, 34, 4, 6, 5, 5, 4, 5, 4, 5, 40, 4, 18827, 3, 5, 2, 42, 6, 5, 1, 1, 34, 5, 6, 5, 4, 5, 5, 5, 4, 41, 5, 18844, 3, 4, 3, 42, 6, 4, 1, 1, 5, 5, 6, 0, 4, 0, 0, 45, 4, 4, 3, 3, 8, 0, 42, 9, 8, 1, 1, 5, 4, 9, 0, 8, 30, 0, 8, 9, 4, 8, 5, 5, 9, 0, 8, 68, 0, 8, 9, 4, 8, 5, 6, 9, 3, 8, 1, 42, 9, 8, 1, 5, 5, 7, 9, 3, 8, 2, 0, 9, 38, 0, 8, 10, 8, 9, 47, 9, 18942, 2, 11, 8, 0, 0, 3, 0, 4, 0, 5, 0, 6, 1, 7, 1, 6, 0, 7, 0, 8, 43, 11, 8, 10, 1, 9, 0, 8, 0, 0, 45, 8, 4, 3, 3, 4, 0, 42, 5, 4, 1, 1, 34, 4, 5, 5, 5, 4, 40, 5, 18975, 3, 4, 1, 42, 6, 4, 1, 1, 34, 4, 6, 5, 5, 4, 5, 4, 5, 40, 4, 18995, 3, 5, 2, 42, 6, 5, 1, 1, 34, 5, 6, 5, 4, 5, 5, 5, 4, 41, 5, 19028, 3, 4, 3, 3, 6, 4, 3, 7, 5, 3, 8, 6, 3, 9, 7, 42, 10, 4, 5, 1, 6, 7, 8, 9, 5, 5, 10, 0, 4, 0, 0, 45, 4, 4, 2, 3, 3, 0, 42, 4, 3, 2, 2, 0, 0, 3, 54, 0, 8, 5, 4, 3, 43, 3, 4, 5, 0, 0, 4, 176, 0, 8, 5, 3, 4, 43, 4, 3, 5, 0, 45, 2, 0, 3, 0, 0, 45, 3, 4, 2, 0, 3, 89, 0, 8, 4, 2, 3, 43, 3, 2, 4, 1, 0, 0, 4, 177, 0, 8, 5, 3, 4, 43, 4, 3, 5, 0, 45, 4, 0, 3, 0, 0, 45, 3, 4, 2, 3, 3, 0, 42, 4, 3, 1, 0, 0, 3, 178, 0, 8, 5, 4, 3, 43, 3, 4, 5, 1, 2, 45, 2, 0, 3, 0, 0, 45, 3, 4, 1, 0, 3, 23, 0, 5, 2, 3, 0, 3, 38, 0, 8, 4, 1, 3, 47, 3, 19220, 2, 8, 8, 0, 0, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 1, 2, 0, 6, 43, 5, 1, 4, 1, 3, 0, 3, 18, 0, 8, 4, 2, 3, 0, 3, 68, 0, 43, 5, 2, 4, 1, 3, 45, 5, 0, 3, 0, 0, 45, 3, 4, 3, 3, 4, 0, 0, 5, 334, 0, 8, 6, 1, 5, 5, 5, 6, 41, 5, 19246, 48, 6, 1, 1, 5, 5, 6, 47, 6, 19281, 2, 11, 8, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 0, 0, 6, 0, 7, 42, 7, 4, 2, 5, 6, 0, 4, 0, 0, 45, 4, 4, 3, 0, 6, 246, 0, 8, 7, 1, 6, 5, 6, 7, 41, 6, 19311, 0, 7, 335, 0, 8, 8, 1, 7, 34, 7, 8, 5, 6, 7, 5, 7, 6, 41, 7, 19336, 0, 6, 299, 0, 8, 8, 1, 6, 0, 6, 336, 0, 26, 9, 8, 6, 5, 7, 9, 5, 6, 7, 41, 6, 19370, 3, 7, 0, 0, 8, 191, 0, 8, 9, 7, 8, 0, 8, 80, 0, 8, 10, 1, 8, 43, 8, 7, 9, 1, 10, 5, 6, 8, 5, 7, 6, 41, 7, 19424, 3, 6, 1, 0, 8, 191, 0, 8, 9, 6, 8, 0, 8, 80, 0, 8, 10, 1, 8, 43, 8, 6, 9, 1, 10, 5, 6, 8, 40, 6, 19421, 0, 8, 249, 0, 8, 9, 1, 8, 34, 8, 9, 5, 6, 8, 5, 7, 6, 34, 6, 7, 40, 6, 19503, 3, 6, 2, 42, 7, 6, 1, 1, 5, 4, 7, 3, 6, 3, 42, 7, 6, 1, 4, 34, 6, 7, 40, 6, 19503, 3, 6, 4, 42, 7, 6, 1, 4, 40, 7, 19471, 5, 6, 4, 39, 19478, 48, 7, 1, 4, 5, 6, 7, 5, 5, 6, 3, 6, 5, 47, 7, 19509, 2, 9, 3, 0, 0, 6, 0, 7, 1, 1, 42, 8, 6, 2, 5, 7, 0, 6, 0, 0, 45, 6, 4, 3, 3, 4, 0, 3, 5, 1, 3, 6, 2, 0, 7, 335, 0, 8, 8, 6, 7, 42, 6, 5, 2, 8, 1, 11, 5, 4, 6, 7, 0, 5, 0, 4, 0, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 4; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/oracle.json new file mode 100644 index 00000000..1612eaf9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/oracle.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "program-cash", + "hostProfile": "cash-dom-test-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [ + { + "op": "typeOf", + "root": "window", + "path": [ + "cash" + ], + "as": "cashType" + }, + { + "op": "keys", + "root": "window", + "path": [ + "cash" + ], + "as": "cashKeys" + } + ], + "timeoutMs": 10000, + "expected": { + "cashType": "function", + "cashKeys": [ + "fn", + "isWindow", + "isFunction", + "isArray", + "isNumeric", + "isPlainObject", + "each", + "extend", + "parseHTML", + "guid", + "unique" + ] + }, + "expectedThrow": null, + "semanticTags": [ + "whole-program", + "host-profile", + "iife", + "object-methods", + "regex", + "mutation" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded.", + "cash-dom-test-v1 copies globalThis names into a fixed DOM/module stub; real DOM and ambient host state are excluded.", + "Post-evaluation observations use only the finite declarative actions recorded in the oracle; function values are never serialized." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/source.js new file mode 100644 index 00000000..a811c2f9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-cash/source.js @@ -0,0 +1,2 @@ +(function(){"use strict";var C=document,D=window,st=C.documentElement,L=C.createElement.bind(C),ft=L("div"),q=L("table"),Mt=L("tbody"),ot=L("tr"),H=Array.isArray,S=Array.prototype,Dt=S.concat,U=S.filter,at=S.indexOf,ct=S.map,Bt=S.push,ht=S.slice,z=S.some,_t=S.splice,Pt=/^#(?:[\w-]|\\.|[^\x00-\xa0])*$/,Ht=/^\.(?:[\w-]|\\.|[^\x00-\xa0])*$/,$t=/<.+>/,jt=/^\w+$/;function J(t,n){var r=It(n);return!t||!r&&!A(n)&&!c(n)?[]:!r&&Ht.test(t)?n.getElementsByClassName(t.slice(1).replace(/\\/g,"")):!r&&jt.test(t)?n.getElementsByTagName(t):n.querySelectorAll(t)}var dt=function(){function t(n,r){if(n){if(Y(n))return n;var i=n;if(g(n)){var e=r||C;if(i=Pt.test(n)&&A(e)?e.getElementById(n.slice(1).replace(/\\/g,"")):$t.test(n)?yt(n):Y(e)?e.find(n):g(e)?o(e).find(n):J(n,e),!i)return}else if(O(n))return this.ready(n);(i.nodeType||i===D)&&(i=[i]),this.length=i.length;for(var s=0,f=this.length;s]*>/,Gt=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,mt={"*":ft,tr:Mt,td:ot,th:ot,thead:q,tbody:q,tfoot:q};function yt(t){if(!g(t))return[];if(Gt.test(t))return[L(RegExp.$1)];var n=Yt.test(t)&&RegExp.$1,r=mt[n]||mt["*"];return r.innerHTML=t,o(r.childNodes).detach().get()}o.parseHTML=yt,u.has=function(t){var n=g(t)?function(r,i){return J(t,i).length}:function(r,i){return i.contains(t)};return this.filter(n)},u.not=function(t){var n=I(t);return this.filter(function(r,i){return(!g(t)||c(i))&&!n.call(i,r,i)})};function R(t,n,r,i){for(var e=[],s=O(n),f=i&&I(i),a=0,y=t.length;a=0},!0):r.checked=e.indexOf(r.value)>=0}else r.value=v(t)||P(t)?"":t}):this[0]&&bt(this[0])}u.val=Xt,u.is=function(t){var n=I(t);return z.call(this,function(r,i){return n.call(r,i,r)})},o.guid=1;function w(t){return t.length>1?U.call(t,function(n,r,i){return at.call(i,n)===r}):t}o.unique=w,u.add=function(t,n){return o(w(this.get().concat(o(t,n).get())))},u.children=function(t){return x(o(w(R(this,function(n){return n.children}))),t)},u.parent=function(t){return x(o(w(R(this,"parentNode"))),t)},u.index=function(t){var n=t?o(t)[0]:this[0],r=t?this:o(n).parent().children();return at.call(r,n)},u.closest=function(t){var n=this.filter(t);if(n.length)return n;var r=this.parent();return r.length?r.closest(t):n},u.siblings=function(t){return x(o(w(R(this,function(n){return o(n).parent().children().not(n)}))),t)},u.find=function(t){return o(w(R(this,function(n){return J(t,n)})))};var Kt=/^\s*\s*$/g,Qt=/^$|^module$|\/(java|ecma)script/i,Vt=["type","src","nonce","noModule"];function Zt(t,n){var r=o(t);r.filter("script").add(r.find("script")).each(function(i,e){if(Qt.test(e.type)&&st.contains(e)){var s=L("script");s.text=e.textContent.replace(Kt,""),d(Vt,function(f,a){e[a]&&(s[a]=e[a])}),n.head.insertBefore(s,null),n.head.removeChild(s)}})}function kt(t,n,r,i,e){i?t.insertBefore(n,r?t.firstChild:null):t.nodeName==="HTML"?t.parentNode.replaceChild(n,t):t.parentNode.insertBefore(n,r?t:t.nextSibling),e&&Zt(n,t.ownerDocument)}function N(t,n,r,i,e,s,f,a){return d(t,function(y,h){d(o(h),function(p,M){d(o(n),function(b,W){var rt=r?M:W,it=r?W:M,m=r?p:b;kt(rt,m?it.cloneNode(!0):it,i,e,!m)},a)},f)},s),n}u.after=function(){return N(arguments,this,!1,!1,!1,!0,!0)},u.append=function(){return N(arguments,this,!1,!1,!0)};function tn(t){if(!arguments.length)return this[0]&&this[0].innerHTML;if(v(t))return this;var n=/]/.test(t);return this.each(function(r,i){c(i)&&(n?o(i).empty().append(t):i.innerHTML=t)})}u.html=tn,u.appendTo=function(t){return N(arguments,this,!0,!1,!0)},u.wrapInner=function(t){return this.each(function(n,r){var i=o(r),e=i.contents();e.length?e.wrapAll(t):i.append(t)})},u.before=function(){return N(arguments,this,!1,!0)},u.wrapAll=function(t){for(var n=o(t),r=n[0];r.children.length;)r=r.firstElementChild;return this.first().before(n),this.appendTo(r)},u.wrap=function(t){return this.each(function(n,r){var i=o(t)[0];o(r).wrapAll(n?i.cloneNode(!0):i)})},u.insertAfter=function(t){return N(arguments,this,!0,!1,!1,!1,!1,!0)},u.insertBefore=function(t){return N(arguments,this,!0,!0)},u.prepend=function(){return N(arguments,this,!1,!0,!0,!0,!0)},u.prependTo=function(t){return N(arguments,this,!0,!0,!0,!1,!1,!0)},u.contents=function(){return o(w(R(this,function(t){return t.tagName==="IFRAME"?[t.contentDocument]:t.tagName==="TEMPLATE"?t.content.childNodes:t.childNodes})))},u.next=function(t,n,r){return x(o(w(R(this,"nextElementSibling",n,r))),t)},u.nextAll=function(t){return this.next(t,!0)},u.nextUntil=function(t,n){return this.next(n,!0,t)},u.parents=function(t,n){return x(o(w(R(this,"parentElement",!0,n))),t)},u.parentsUntil=function(t,n){return this.parents(n,t)},u.prev=function(t,n,r){return x(o(w(R(this,"previousElementSibling",n,r))),t)},u.prevAll=function(t){return this.prev(t,!0)},u.prevUntil=function(t,n){return this.prev(n,!0,t)},u.map=function(t){return o(Dt.apply([],ct.call(this,function(n,r){return t.call(n,r,n)})))},u.clone=function(){return this.map(function(t,n){return n.cloneNode(!0)})},u.offsetParent=function(){return this.map(function(t,n){for(var r=n.offsetParent;r&&T(r,"position")==="static";)r=r.offsetParent;return r||st})},u.slice=function(t,n){return o(ht.call(this,t,n))};var nn=/-([a-z])/g;function K(t){return t.replace(nn,function(n,r){return r.toUpperCase()})}u.ready=function(t){var n=function(){return setTimeout(t,0,o)};return C.readyState!=="loading"?n():C.addEventListener("DOMContentLoaded",n),this},u.unwrap=function(){return this.parent().each(function(t,n){if(n.tagName!=="BODY"){var r=o(n);r.replaceWith(r.children())}}),this},u.offset=function(){var t=this[0];if(t){var n=t.getBoundingClientRect();return{top:n.top+D.pageYOffset,left:n.left+D.pageXOffset}}},u.position=function(){var t=this[0];if(t){var n=T(t,"position")==="fixed",r=n?t.getBoundingClientRect():this.offset();if(!n){for(var i=t.ownerDocument,e=t.offsetParent||i.documentElement;(e===i.body||e===i.documentElement)&&T(e,"position")==="static";)e=e.parentNode;if(e!==t&&c(e)){var s=o(e).offset();r.top-=s.top+E(e,"borderTopWidth"),r.left-=s.left+E(e,"borderLeftWidth")}}return{top:r.top-E(t,"marginTop"),left:r.left-E(t,"marginLeft")}}};var Et={class:"className",contenteditable:"contentEditable",for:"htmlFor",readonly:"readOnly",maxlength:"maxLength",tabindex:"tabIndex",colspan:"colSpan",rowspan:"rowSpan",usemap:"useMap"};u.prop=function(t,n){if(t){if(g(t))return t=Et[t]||t,arguments.length<2?this[0]&&this[0][t]:this.each(function(i,e){e[t]=n});for(var r in t)this.prop(r,t[r]);return this}},u.removeProp=function(t){return this.each(function(n,r){delete r[Et[t]||t]})};var rn=/^--/;function Q(t){return rn.test(t)}var V={},en=ft.style,un=["webkit","moz","ms"];function sn(t,n){if(n===void 0&&(n=Q(t)),n)return t;if(!V[t]){var r=K(t),i="".concat(r[0].toUpperCase()).concat(r.slice(1)),e="".concat(r," ").concat(un.join("".concat(i," "))).concat(i).split(" ");d(e,function(s,f){if(f in en)return V[t]=f,!1})}return V[t]}var fn={animationIterationCount:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0};function wt(t,n,r){return r===void 0&&(r=Q(t)),!r&&!fn[t]&<(n)?"".concat(n,"px"):n}function on(t,n){if(g(t)){var r=Q(t);return t=sn(t,r),arguments.length<2?this[0]&&T(this[0],t,r):t?(n=wt(t,n,r),this.each(function(e,s){c(s)&&(r?s.style.setProperty(t,n):s.style[t]=n)})):this}for(var i in t)this.css(i,t[i]);return this}u.css=on;function Ct(t,n){try{return t(n)}catch{return n}}var an=/^\s+|\s+$/;function St(t,n){var r=t.dataset[n]||t.dataset[K(n)];return an.test(r)?r:Ct(JSON.parse,r)}function cn(t,n,r){r=Ct(JSON.stringify,r),t.dataset[K(n)]=r}function hn(t,n){if(!t){if(!this[0])return;var r={};for(var i in this[0].dataset)r[i]=St(this[0],i);return r}if(g(t))return arguments.length<2?this[0]&&St(this[0],t):v(n)?this:this.each(function(e,s){cn(s,t,n)});for(var i in t)this.data(i,t[i]);return this}u.data=hn;function Tt(t,n){var r=t.documentElement;return Math.max(t.body["scroll".concat(n)],r["scroll".concat(n)],t.body["offset".concat(n)],r["offset".concat(n)],r["client".concat(n)])}d([!0,!1],function(t,n){d(["Width","Height"],function(r,i){var e="".concat(n?"outer":"inner").concat(i);u[e]=function(s){if(this[0])return B(this[0])?n?this[0]["inner".concat(i)]:this[0].document.documentElement["client".concat(i)]:A(this[0])?Tt(this[0],i):this[0]["".concat(n?"offset":"client").concat(i)]+(s&&n?E(this[0],"margin".concat(r?"Top":"Left"))+E(this[0],"margin".concat(r?"Bottom":"Right")):0)}})}),d(["Width","Height"],function(t,n){var r=n.toLowerCase();u[r]=function(i){if(!this[0])return v(i)?void 0:this;if(!arguments.length)return B(this[0])?this[0].document.documentElement["client".concat(n)]:A(this[0])?Tt(this[0],n):this[0].getBoundingClientRect()[r]-gt(this[0],!t);var e=parseInt(i,10);return this.each(function(s,f){if(c(f)){var a=T(f,"boxSizing");f.style[r]=wt(r,e+(a==="border-box"?gt(f,!t):0))}})}});var Rt="___cd";u.toggle=function(t){return this.each(function(n,r){if(c(r)){var i=vt(r),e=v(t)?i:t;e?(r.style.display=r[Rt]||"",vt(r)&&(r.style.display=Jt(r.tagName))):i||(r[Rt]=T(r,"display"),r.style.display="none")}})},u.hide=function(){return this.toggle(!1)},u.show=function(){return this.toggle(!0)};var xt="___ce",Z=".",k={focus:"focusin",blur:"focusout"},Nt={mouseenter:"mouseover",mouseleave:"mouseout"},dn=/^(mouse|pointer|contextmenu|drag|drop|click|dblclick)/i;function tt(t){return Nt[t]||k[t]||t}function nt(t){var n=t.split(Z);return[n[0],n.slice(1).sort()]}u.trigger=function(t,n){if(g(t)){var r=nt(t),i=r[0],e=r[1],s=tt(i);if(!s)return this;var f=dn.test(s)?"MouseEvents":"HTMLEvents";t=C.createEvent(f),t.initEvent(s,!0,!0),t.namespace=e.join(Z),t.___ot=i}t.___td=n;var a=t.___ot in k;return this.each(function(y,h){a&&O(h[t.___ot])&&(h["___i".concat(t.type)]=!0,h[t.___ot](),h["___i".concat(t.type)]=!1),h.dispatchEvent(t)})};function Lt(t){return t[xt]=t[xt]||{}}function ln(t,n,r,i,e){var s=Lt(t);s[n]=s[n]||[],s[n].push([r,i,e]),t.addEventListener(n,e)}function At(t,n){return!n||!z.call(n,function(r){return t.indexOf(r)<0})}function F(t,n,r,i,e){var s=Lt(t);if(n)s[n]&&(s[n]=s[n].filter(function(f){var a=f[0],y=f[1],h=f[2];if(e&&h.guid!==e.guid||!At(a,r)||i&&i!==y)return!0;t.removeEventListener(n,h)}));else for(n in s)F(t,n,r,i,e)}u.off=function(t,n,r){var i=this;if(v(t))this.each(function(s,f){!c(f)&&!A(f)&&!B(f)||F(f)});else if(g(t))O(n)&&(r=n,n=""),d(j(t),function(s,f){var a=nt(f),y=a[0],h=a[1],p=tt(y);i.each(function(M,b){!c(b)&&!A(b)&&!B(b)||F(b,p,h,n,r)})});else for(var e in t)this.off(e,t[e]);return this},u.remove=function(t){return x(this,t).detach().off(),this},u.replaceWith=function(t){return this.before(t).remove()},u.replaceAll=function(t){return o(t).replaceWith(this),this};function gn(t,n,r,i,e){var s=this;if(!g(t)){for(var f in t)this.on(f,n,r,t[f],e);return this}return g(n)||(v(n)||P(n)?n="":v(r)?(r=n,n=""):(i=r,r=n,n="")),O(i)||(i=r,r=void 0),i?(d(j(t),function(a,y){var h=nt(y),p=h[0],M=h[1],b=tt(p),W=p in Nt,rt=p in k;b&&s.each(function(it,m){if(!(!c(m)&&!A(m)&&!B(m))){var et=function(l){if(l.target["___i".concat(l.type)])return l.stopImmediatePropagation();if(!(l.namespace&&!At(M,l.namespace.split(Z)))&&!(!n&&(rt&&(l.target!==m||l.___ot===b)||W&&l.relatedTarget&&m.contains(l.relatedTarget)))){var ut=m;if(n){for(var _=l.target;!pt(_,n);)if(_===m||(_=_.parentNode,!_))return;ut=_}Object.defineProperty(l,"currentTarget",{configurable:!0,get:function(){return ut}}),Object.defineProperty(l,"delegateTarget",{configurable:!0,get:function(){return m}}),Object.defineProperty(l,"data",{configurable:!0,get:function(){return r}});var bn=i.call(ut,l,l.___td);e&&F(m,b,M,n,et),bn===!1&&(l.preventDefault(),l.stopPropagation())}};et.guid=i.guid=i.guid||o.guid++,ln(m,b,M,n,et)}})}),this):this}u.on=gn;function vn(t,n,r,i){return this.on(t,n,r,i,!0)}u.one=vn;var pn=/\r?\n/g;function mn(t,n){return"&".concat(encodeURIComponent(t),"=").concat(encodeURIComponent(n.replace(pn,`\r +`)))}var yn=/file|reset|submit|button|image/i,Ot=/radio|checkbox/i;u.serialize=function(){var t="";return this.each(function(n,r){d(r.elements||[r],function(i,e){if(!(e.disabled||!e.name||e.tagName==="FIELDSET"||yn.test(e.type)||Ot.test(e.type)&&!e.checked)){var s=bt(e);if(!v(s)){var f=H(s)?s:[s];d(f,function(a,y){t+=mn(e.name,y)})}}})}),t.slice(1)},typeof exports<"u"?module.exports=o:D.cash=D.$=o})(); diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/encoded.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/encoded.js new file mode 100644 index 00000000..99829744 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"module", /* 1 */"undefined", /* 2 */"exports", /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */undefined, /* 6 */0, /* 7 */"", /* 8 */"Array", /* 9 */1116352408, /* 10 */1899447441, /* 11 */1245643825, /* 12 */373957723, /* 13 */961987163, /* 14 */1508970993, /* 15 */1841331548, /* 16 */1424204075, /* 17 */670586216, /* 18 */310598401, /* 19 */607225278, /* 20 */1426881987, /* 21 */1925078388, /* 22 */2132889090, /* 23 */1680079193, /* 24 */1046744716, /* 25 */459576895, /* 26 */272742522, /* 27 */264347078, /* 28 */604807628, /* 29 */770255983, /* 30 */1249150122, /* 31 */1555081692, /* 32 */1996064986, /* 33 */1740746414, /* 34 */1473132947, /* 35 */1341970488, /* 36 */1084653625, /* 37 */958395405, /* 38 */710438585, /* 39 */113926993, /* 40 */338241895, /* 41 */666307205, /* 42 */773529912, /* 43 */1294757372, /* 44 */1396182291, /* 45 */1695183700, /* 46 */1986661051, /* 47 */2117940946, /* 48 */1838011259, /* 49 */1564481375, /* 50 */1474664885, /* 51 */1035236496, /* 52 */949202525, /* 53 */778901479, /* 54 */694614492, /* 55 */200395387, /* 56 */275423344, /* 57 */430227734, /* 58 */506948616, /* 59 */659060556, /* 60 */883997877, /* 61 */958139571, /* 62 */1322822218, /* 63 */1537002063, /* 64 */1747873779, /* 65 */1955562222, /* 66 */2024104815, /* 67 */2067236844, /* 68 */1933114872, /* 69 */1866530822, /* 70 */1538233109, /* 71 */1090935817, /* 72 */965641998, /* 73 */"hex", /* 74 */"b64", /* 75 */"any", /* 76 */"hex_hmac", /* 77 */"b64_hmac", /* 78 */"any_hmac", /* 79 */"abc", /* 80 */"toLowerCase", /* 81 */"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", /* 82 */"length", /* 83 */8, /* 84 */16, /* 85 */909522486, /* 86 */1549556828, /* 87 */1, /* 88 */"concat", /* 89 */512, /* 90 */256, /* 91 */"0123456789ABCDEF", /* 92 */"0123456789abcdef", /* 93 */"charCodeAt", /* 94 */"charAt", /* 95 */4, /* 96 */15, /* 97 */"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", /* 98 */2, /* 99 */6, /* 100 */3, /* 101 */63, /* 102 */"Math", /* 103 */"ceil", /* 104 */"floor", /* 105 */"log", /* 106 */55296, /* 107 */56319, /* 108 */56320, /* 109 */57343, /* 110 */65536, /* 111 */1023, /* 112 */10, /* 113 */127, /* 114 */"String", /* 115 */"fromCharCode", /* 116 */2047, /* 117 */192, /* 118 */31, /* 119 */128, /* 120 */65535, /* 121 */224, /* 122 */12, /* 123 */2097151, /* 124 */240, /* 125 */18, /* 126 */7, /* 127 */255, /* 128 */5, /* 129 */24, /* 130 */32, /* 131 */13, /* 132 */22, /* 133 */11, /* 134 */25, /* 135 */17, /* 136 */19, /* 137 */28, /* 138 */34, /* 139 */39, /* 140 */14, /* 141 */41, /* 142 */61, /* 143 */1779033703, /* 144 */1150833019, /* 145 */1013904242, /* 146 */1521486534, /* 147 */1359893119, /* 148 */1694144372, /* 149 */528734635, /* 150 */1541459225, /* 151 */64, /* 152 */9]; +var BYTECODE = [4, 1, 47, 3, 76, 0, 102, 0, 0, 42, 4, 3, 0, 5, 2, 4, 38, 3, 0, 0, 0, 4, 1, 0, 27, 5, 3, 4, 40, 5, 43, 2, 3, 0, 0, 0, 4, 2, 0, 9, 3, 4, 2, 38, 3, 3, 0, 0, 4, 1, 0, 27, 5, 3, 4, 40, 5, 70, 2, 3, 3, 0, 0, 4, 4, 0, 9, 3, 4, 2, 0, 3, 5, 0, 45, 3, 4, 1, 47, 36, 967, 1, 7, 3, 0, 1, 13, 1, 11, 1, 16, 5, 4, 36, 47, 36, 1001, 1, 7, 3, 0, 1, 14, 1, 11, 1, 16, 5, 5, 36, 47, 36, 1035, 2, 8, 3, 0, 1, 15, 1, 11, 1, 16, 5, 6, 36, 47, 36, 1070, 2, 9, 3, 0, 1, 13, 1, 12, 1, 16, 5, 7, 36, 47, 36, 1113, 2, 9, 3, 0, 1, 14, 1, 12, 1, 16, 5, 8, 36, 47, 36, 1156, 3, 10, 3, 0, 1, 15, 1, 12, 1, 16, 5, 9, 36, 47, 36, 1200, 0, 5, 1, 0, 1, 4, 5, 10, 36, 47, 36, 1243, 1, 9, 3, 0, 1, 20, 1, 34, 1, 19, 5, 11, 36, 47, 36, 1294, 2, 15, 3, 0, 1, 19, 1, 34, 1, 20, 5, 12, 36, 47, 36, 1561, 1, 12, 1, 0, 1, 2, 5, 13, 36, 47, 36, 1741, 1, 14, 1, 0, 1, 3, 5, 14, 36, 47, 36, 2096, 2, 21, 0, 0, 5, 15, 36, 47, 36, 2711, 1, 15, 0, 0, 5, 16, 36, 47, 36, 3314, 1, 11, 0, 0, 5, 17, 36, 47, 36, 3447, 1, 11, 0, 0, 5, 18, 36, 47, 36, 3580, 1, 11, 0, 0, 5, 19, 36, 47, 36, 3777, 1, 11, 0, 0, 5, 20, 36, 47, 36, 3902, 2, 7, 0, 0, 5, 21, 36, 47, 36, 3932, 2, 5, 0, 0, 5, 22, 36, 47, 36, 3946, 3, 8, 0, 0, 5, 23, 36, 47, 36, 3971, 3, 8, 0, 0, 5, 24, 36, 47, 36, 4001, 1, 7, 1, 0, 1, 21, 5, 25, 36, 47, 36, 4058, 1, 7, 1, 0, 1, 21, 5, 26, 36, 47, 36, 4115, 1, 7, 2, 0, 1, 21, 1, 22, 5, 27, 36, 47, 36, 4172, 1, 7, 2, 0, 1, 21, 1, 22, 5, 28, 36, 47, 36, 4229, 1, 7, 1, 0, 1, 21, 5, 29, 36, 47, 36, 4286, 1, 7, 1, 0, 1, 21, 5, 30, 36, 47, 36, 4343, 1, 7, 2, 0, 1, 21, 1, 22, 5, 31, 36, 47, 36, 4400, 1, 7, 2, 0, 1, 21, 1, 22, 5, 32, 36, 47, 36, 4457, 2, 28, 8, 0, 1, 35, 1, 28, 1, 27, 1, 26, 1, 23, 1, 33, 1, 25, 1, 24, 5, 34, 36, 47, 36, 5288, 2, 9, 0, 0, 5, 35, 36, 0, 36, 6, 0, 5, 2, 36, 0, 36, 7, 0, 5, 3, 36, 2, 36, 8, 0, 0, 37, 9, 0, 0, 38, 10, 0, 0, 39, 11, 0, 32, 40, 39, 0, 39, 12, 0, 32, 41, 39, 0, 39, 13, 0, 0, 42, 14, 0, 0, 43, 15, 0, 32, 44, 43, 0, 43, 16, 0, 32, 45, 43, 0, 43, 17, 0, 32, 46, 43, 0, 43, 18, 0, 0, 47, 19, 0, 0, 48, 20, 0, 0, 49, 21, 0, 0, 50, 22, 0, 32, 51, 50, 0, 50, 23, 0, 32, 52, 50, 0, 50, 24, 0, 32, 53, 50, 0, 50, 25, 0, 32, 54, 50, 0, 50, 26, 0, 32, 55, 50, 0, 50, 27, 0, 0, 56, 28, 0, 0, 57, 29, 0, 0, 58, 30, 0, 0, 59, 31, 0, 0, 60, 32, 0, 0, 61, 33, 0, 32, 62, 61, 0, 61, 34, 0, 32, 63, 61, 0, 61, 35, 0, 32, 64, 61, 0, 61, 36, 0, 32, 65, 61, 0, 61, 37, 0, 32, 66, 61, 0, 61, 38, 0, 32, 67, 61, 0, 61, 39, 0, 0, 68, 40, 0, 0, 69, 41, 0, 0, 70, 42, 0, 0, 71, 43, 0, 0, 72, 44, 0, 0, 73, 45, 0, 0, 74, 46, 0, 0, 75, 47, 0, 32, 76, 75, 0, 75, 48, 0, 32, 77, 75, 0, 75, 49, 0, 32, 78, 75, 0, 75, 50, 0, 32, 79, 75, 0, 75, 51, 0, 32, 80, 75, 0, 75, 52, 0, 32, 81, 75, 0, 75, 53, 0, 32, 82, 75, 0, 75, 54, 0, 32, 83, 75, 0, 75, 55, 0, 32, 84, 75, 0, 75, 56, 0, 0, 85, 57, 0, 0, 86, 58, 0, 0, 87, 59, 0, 0, 88, 60, 0, 0, 89, 61, 0, 0, 90, 62, 0, 0, 91, 63, 0, 0, 92, 64, 0, 0, 93, 65, 0, 0, 94, 66, 0, 0, 95, 67, 0, 32, 96, 95, 0, 95, 68, 0, 32, 97, 95, 0, 95, 69, 0, 32, 98, 95, 0, 95, 70, 0, 32, 99, 95, 0, 95, 71, 0, 32, 100, 95, 0, 95, 72, 0, 32, 101, 95, 44, 95, 36, 64, 37, 38, 40, 41, 39, 42, 44, 45, 46, 43, 47, 48, 49, 51, 52, 53, 54, 55, 50, 56, 57, 58, 59, 60, 62, 63, 64, 65, 66, 67, 61, 68, 69, 70, 71, 72, 73, 74, 76, 77, 78, 79, 80, 81, 82, 83, 84, 75, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 96, 97, 98, 99, 100, 101, 5, 33, 95, 0, 36, 73, 0, 0, 37, 74, 0, 0, 38, 75, 0, 0, 39, 76, 0, 0, 40, 77, 0, 0, 41, 78, 0, 49, 42, 6, 36, 4, 37, 8, 38, 9, 39, 7, 40, 8, 41, 9, 45, 42, 0, 36, 5, 0, 45, 36, 4, 2, 3, 3, 0, 3, 4, 1, 3, 5, 2, 42, 6, 5, 1, 0, 42, 5, 4, 1, 6, 42, 4, 3, 1, 5, 45, 4, 0, 3, 5, 0, 45, 3, 4, 2, 3, 3, 0, 3, 4, 1, 3, 5, 2, 42, 6, 5, 1, 0, 42, 5, 4, 1, 6, 42, 4, 3, 1, 5, 45, 4, 0, 3, 5, 0, 45, 3, 4, 3, 3, 4, 0, 3, 5, 1, 3, 6, 2, 42, 7, 6, 1, 0, 42, 6, 5, 1, 7, 42, 5, 4, 2, 6, 1, 45, 5, 0, 4, 5, 0, 45, 4, 4, 3, 3, 4, 0, 3, 5, 1, 3, 6, 2, 42, 7, 6, 1, 0, 3, 6, 2, 42, 8, 6, 1, 1, 42, 6, 5, 2, 7, 8, 42, 5, 4, 1, 6, 45, 5, 0, 4, 5, 0, 45, 4, 4, 3, 3, 4, 0, 3, 5, 1, 3, 6, 2, 42, 7, 6, 1, 0, 3, 6, 2, 42, 8, 6, 1, 1, 42, 6, 5, 2, 7, 8, 42, 5, 4, 1, 6, 45, 5, 0, 4, 5, 0, 45, 4, 4, 4, 3, 5, 0, 3, 6, 1, 3, 7, 2, 42, 8, 7, 1, 0, 3, 7, 2, 42, 9, 7, 1, 1, 42, 7, 6, 2, 8, 9, 42, 6, 5, 2, 7, 2, 45, 6, 0, 5, 5, 0, 45, 5, 4, 1, 3, 2, 0, 0, 3, 79, 0, 42, 4, 2, 1, 3, 0, 2, 80, 0, 8, 3, 4, 2, 43, 2, 4, 3, 0, 0, 3, 81, 0, 28, 4, 2, 3, 45, 4, 0, 2, 5, 0, 45, 2, 4, 2, 3, 3, 0, 3, 4, 1, 3, 5, 2, 42, 6, 5, 1, 0, 0, 5, 82, 0, 8, 7, 0, 5, 0, 5, 83, 0, 13, 8, 7, 5, 42, 5, 4, 2, 6, 8, 42, 4, 3, 1, 5, 45, 4, 0, 3, 5, 0, 45, 3, 4, 3, 3, 9, 0, 42, 10, 9, 1, 0, 5, 4, 10, 0, 9, 82, 0, 8, 10, 4, 9, 0, 9, 84, 0, 23, 11, 10, 9, 40, 11, 1354, 3, 9, 1, 0, 10, 82, 0, 8, 11, 0, 10, 0, 10, 83, 0, 13, 12, 11, 10, 42, 10, 9, 2, 4, 12, 5, 4, 10, 2, 9, 8, 0, 0, 10, 84, 0, 42, 11, 9, 1, 10, 5, 5, 11, 2, 9, 8, 0, 0, 10, 84, 0, 42, 11, 9, 1, 10, 5, 6, 11, 0, 9, 6, 0, 5, 7, 9, 0, 9, 84, 0, 22, 10, 7, 9, 40, 10, 1452, 8, 9, 4, 7, 0, 10, 85, 0, 18, 11, 9, 10, 9, 5, 7, 11, 8, 9, 4, 7, 0, 10, 86, 0, 18, 11, 9, 10, 9, 6, 7, 11, 5, 9, 7, 0, 9, 87, 0, 11, 10, 7, 9, 5, 7, 10, 39, 1393, 3, 9, 1, 0, 10, 88, 0, 8, 11, 5, 10, 3, 10, 0, 42, 12, 10, 1, 1, 43, 10, 5, 11, 1, 12, 0, 11, 89, 0, 0, 12, 82, 0, 8, 13, 1, 12, 0, 12, 83, 0, 13, 14, 13, 12, 11, 12, 11, 14, 42, 11, 9, 2, 10, 12, 5, 8, 11, 3, 9, 2, 3, 10, 1, 0, 11, 88, 0, 8, 12, 6, 11, 43, 11, 6, 12, 1, 8, 0, 12, 89, 0, 0, 13, 90, 0, 11, 14, 12, 13, 42, 12, 10, 2, 11, 14, 42, 10, 9, 1, 12, 45, 10, 0, 9, 5, 0, 45, 9, 4, 2, 54, 1572, 3, 3, 8, 0, 55, 39, 1579, 0, 8, 6, 0, 7, 0, 8, 3, 8, 0, 40, 8, 1594, 0, 8, 91, 0, 5, 9, 8, 39, 1601, 0, 8, 92, 0, 5, 9, 8, 5, 4, 9, 0, 8, 7, 0, 5, 5, 8, 0, 6, 5, 0, 0, 8, 6, 0, 5, 7, 8, 0, 8, 82, 0, 8, 9, 0, 8, 22, 8, 7, 9, 40, 8, 1733, 0, 8, 93, 0, 8, 9, 0, 8, 43, 8, 0, 9, 1, 7, 5, 6, 8, 0, 8, 94, 0, 8, 9, 4, 8, 0, 8, 95, 0, 21, 10, 6, 8, 0, 8, 96, 0, 16, 11, 10, 8, 43, 8, 4, 9, 1, 11, 0, 9, 94, 0, 8, 10, 4, 9, 0, 9, 96, 0, 16, 11, 6, 9, 43, 9, 4, 10, 1, 11, 11, 10, 8, 9, 11, 8, 5, 10, 5, 5, 8, 5, 8, 7, 0, 8, 87, 0, 11, 9, 7, 8, 5, 7, 9, 39, 1622, 45, 5, 0, 8, 5, 0, 45, 8, 4, 2, 54, 1752, 3, 3, 10, 0, 55, 39, 1759, 0, 10, 7, 0, 7, 0, 10, 0, 10, 97, 0, 5, 4, 10, 0, 10, 7, 0, 5, 5, 10, 0, 10, 82, 0, 8, 11, 0, 10, 5, 6, 11, 0, 10, 6, 0, 5, 7, 10, 22, 10, 7, 6, 40, 10, 2088, 0, 10, 93, 0, 8, 11, 0, 10, 43, 10, 0, 11, 1, 7, 0, 11, 84, 0, 19, 12, 10, 11, 0, 10, 87, 0, 11, 11, 7, 10, 22, 10, 11, 6, 40, 10, 1870, 0, 10, 93, 0, 8, 11, 0, 10, 0, 10, 87, 0, 11, 13, 7, 10, 43, 10, 0, 11, 1, 13, 0, 11, 83, 0, 19, 13, 10, 11, 5, 10, 13, 39, 1877, 0, 11, 6, 0, 5, 10, 11, 17, 11, 12, 10, 0, 10, 98, 0, 11, 12, 7, 10, 22, 10, 12, 6, 40, 10, 1923, 0, 10, 93, 0, 8, 12, 0, 10, 0, 10, 98, 0, 11, 13, 7, 10, 43, 10, 0, 12, 1, 13, 5, 12, 10, 39, 1930, 0, 10, 6, 0, 5, 12, 10, 17, 10, 11, 12, 5, 8, 10, 0, 10, 6, 0, 5, 9, 10, 0, 10, 95, 0, 22, 11, 9, 10, 40, 11, 2075, 0, 10, 83, 0, 13, 11, 7, 10, 0, 10, 99, 0, 13, 12, 9, 10, 11, 10, 11, 12, 0, 11, 82, 0, 8, 12, 0, 11, 0, 11, 83, 0, 13, 13, 12, 11, 23, 11, 10, 13, 40, 11, 2010, 3, 10, 0, 11, 11, 5, 10, 5, 5, 11, 39, 2059, 0, 10, 94, 0, 8, 11, 4, 10, 0, 10, 99, 0, 0, 12, 100, 0, 12, 13, 12, 9, 13, 12, 10, 13, 21, 10, 8, 12, 0, 12, 101, 0, 16, 13, 10, 12, 43, 10, 4, 11, 1, 13, 11, 11, 5, 10, 5, 5, 11, 5, 10, 9, 0, 10, 87, 0, 11, 11, 9, 10, 5, 9, 11, 39, 1944, 0, 10, 100, 0, 11, 11, 7, 10, 5, 7, 11, 39, 1791, 45, 5, 0, 10, 5, 0, 45, 10, 4, 3, 0, 13, 82, 0, 8, 14, 1, 13, 5, 4, 14, 2, 13, 8, 0, 42, 14, 13, 0, 5, 5, 14, 0, 6, 5, 0, 0, 7, 5, 0, 0, 8, 5, 0, 0, 9, 5, 0, 2, 13, 8, 0, 2, 14, 102, 0, 0, 15, 103, 0, 8, 16, 14, 15, 0, 15, 82, 0, 8, 17, 0, 15, 0, 15, 98, 0, 14, 18, 17, 15, 43, 15, 14, 16, 1, 18, 42, 14, 13, 1, 15, 5, 10, 14, 0, 13, 6, 0, 5, 6, 13, 0, 13, 82, 0, 8, 14, 10, 13, 22, 13, 6, 14, 40, 13, 2288, 0, 13, 93, 0, 8, 14, 0, 13, 0, 13, 98, 0, 13, 15, 6, 13, 43, 13, 0, 14, 1, 15, 0, 14, 83, 0, 19, 15, 13, 14, 0, 13, 93, 0, 8, 14, 0, 13, 0, 13, 98, 0, 13, 16, 6, 13, 0, 13, 87, 0, 11, 17, 16, 13, 43, 13, 0, 14, 1, 17, 17, 14, 15, 13, 9, 10, 6, 14, 5, 13, 6, 0, 13, 87, 0, 11, 14, 6, 13, 5, 6, 14, 39, 2189, 0, 13, 82, 0, 8, 14, 10, 13, 0, 13, 6, 0, 23, 15, 14, 13, 40, 15, 2483, 2, 13, 8, 0, 42, 14, 13, 0, 5, 9, 14, 0, 13, 6, 0, 5, 8, 13, 0, 13, 6, 0, 5, 6, 13, 0, 13, 82, 0, 8, 14, 10, 13, 22, 13, 6, 14, 40, 13, 2466, 0, 13, 84, 0, 19, 14, 8, 13, 8, 13, 10, 6, 11, 15, 14, 13, 5, 8, 15, 2, 13, 102, 0, 0, 14, 104, 0, 8, 15, 13, 14, 14, 14, 8, 4, 43, 16, 13, 15, 1, 14, 5, 7, 16, 13, 13, 7, 4, 12, 14, 8, 13, 5, 8, 14, 0, 13, 82, 0, 8, 14, 9, 13, 0, 13, 6, 0, 23, 15, 14, 13, 5, 13, 15, 41, 13, 2435, 0, 14, 6, 0, 23, 15, 7, 14, 5, 13, 15, 40, 13, 2450, 0, 13, 82, 0, 8, 14, 9, 13, 9, 9, 14, 7, 5, 13, 6, 0, 13, 87, 0, 11, 14, 6, 13, 5, 6, 14, 39, 2332, 0, 13, 82, 0, 8, 14, 5, 13, 9, 5, 14, 8, 5, 10, 9, 39, 2288, 0, 13, 7, 0, 5, 11, 13, 0, 13, 82, 0, 8, 14, 5, 13, 0, 13, 87, 0, 12, 15, 14, 13, 5, 6, 15, 0, 13, 6, 0, 25, 14, 6, 13, 40, 14, 2561, 0, 13, 94, 0, 8, 14, 1, 13, 8, 13, 5, 6, 43, 15, 1, 14, 1, 13, 11, 13, 11, 15, 5, 11, 13, 5, 13, 6, 0, 13, 87, 0, 12, 14, 6, 13, 5, 6, 14, 39, 2509, 2, 13, 102, 0, 0, 14, 103, 0, 8, 15, 13, 14, 0, 14, 82, 0, 8, 16, 0, 14, 0, 14, 83, 0, 13, 17, 16, 14, 2, 14, 102, 0, 0, 16, 105, 0, 8, 18, 14, 16, 0, 16, 82, 0, 8, 19, 1, 16, 43, 16, 14, 18, 1, 19, 2, 14, 102, 0, 0, 18, 105, 0, 8, 19, 14, 18, 0, 18, 98, 0, 43, 20, 14, 19, 1, 18, 14, 14, 16, 20, 14, 16, 17, 14, 43, 14, 13, 15, 1, 16, 5, 12, 14, 0, 13, 82, 0, 8, 14, 11, 13, 5, 6, 14, 22, 13, 6, 12, 40, 13, 2703, 0, 13, 6, 0, 8, 14, 1, 13, 11, 13, 14, 11, 5, 11, 13, 5, 13, 6, 0, 13, 87, 0, 11, 14, 6, 13, 5, 6, 14, 39, 2665, 45, 11, 0, 13, 5, 0, 45, 13, 4, 2, 0, 7, 7, 0, 5, 3, 7, 0, 7, 87, 0, 32, 8, 7, 5, 4, 8, 0, 5, 5, 0, 0, 6, 5, 0, 0, 7, 87, 0, 11, 8, 4, 7, 5, 4, 8, 0, 7, 82, 0, 8, 9, 0, 7, 22, 7, 8, 9, 40, 7, 3306, 0, 7, 93, 0, 8, 8, 0, 7, 43, 7, 0, 8, 1, 4, 5, 5, 7, 0, 7, 87, 0, 11, 8, 4, 7, 0, 7, 82, 0, 8, 9, 0, 7, 22, 7, 8, 9, 40, 7, 2831, 0, 7, 93, 0, 8, 8, 0, 7, 0, 7, 87, 0, 11, 9, 4, 7, 43, 7, 0, 8, 1, 9, 5, 8, 7, 39, 2838, 0, 7, 6, 0, 5, 8, 7, 5, 6, 8, 0, 7, 106, 0, 24, 8, 7, 5, 5, 7, 8, 40, 7, 2866, 0, 8, 107, 0, 24, 9, 5, 8, 5, 7, 9, 5, 8, 7, 40, 8, 2883, 0, 7, 108, 0, 24, 9, 7, 6, 5, 8, 9, 5, 7, 8, 40, 7, 2900, 0, 8, 109, 0, 24, 9, 6, 8, 5, 7, 9, 40, 7, 2956, 0, 7, 110, 0, 0, 8, 111, 0, 16, 9, 5, 8, 0, 8, 112, 0, 19, 10, 9, 8, 11, 8, 7, 10, 0, 7, 111, 0, 16, 9, 6, 7, 11, 7, 8, 9, 5, 5, 7, 5, 7, 4, 0, 7, 87, 0, 11, 8, 4, 7, 5, 4, 8, 0, 7, 113, 0, 24, 8, 5, 7, 40, 8, 2994, 2, 7, 114, 0, 0, 8, 115, 0, 8, 9, 7, 8, 43, 8, 7, 9, 1, 5, 11, 7, 3, 8, 5, 3, 7, 39, 3304, 0, 7, 116, 0, 24, 8, 5, 7, 40, 8, 3073, 2, 7, 114, 0, 0, 8, 115, 0, 8, 9, 7, 8, 0, 8, 117, 0, 0, 10, 99, 0, 21, 11, 5, 10, 0, 10, 118, 0, 16, 12, 11, 10, 17, 10, 8, 12, 0, 8, 119, 0, 0, 11, 101, 0, 16, 12, 5, 11, 17, 11, 8, 12, 43, 8, 7, 9, 2, 10, 11, 11, 7, 3, 8, 5, 3, 7, 39, 3304, 0, 7, 120, 0, 24, 8, 5, 7, 40, 8, 3177, 2, 7, 114, 0, 0, 8, 115, 0, 8, 9, 7, 8, 0, 8, 121, 0, 0, 10, 122, 0, 21, 11, 5, 10, 0, 10, 96, 0, 16, 12, 11, 10, 17, 10, 8, 12, 0, 8, 119, 0, 0, 11, 99, 0, 21, 12, 5, 11, 0, 11, 101, 0, 16, 13, 12, 11, 17, 11, 8, 13, 0, 8, 119, 0, 0, 12, 101, 0, 16, 13, 5, 12, 17, 12, 8, 13, 43, 8, 7, 9, 3, 10, 11, 12, 11, 7, 3, 8, 5, 3, 7, 39, 3304, 0, 7, 123, 0, 24, 8, 5, 7, 40, 8, 3304, 2, 7, 114, 0, 0, 8, 115, 0, 8, 9, 7, 8, 0, 8, 124, 0, 0, 10, 125, 0, 21, 11, 5, 10, 0, 10, 126, 0, 16, 12, 11, 10, 17, 10, 8, 12, 0, 8, 119, 0, 0, 11, 122, 0, 21, 12, 5, 11, 0, 11, 101, 0, 16, 13, 12, 11, 17, 11, 8, 13, 0, 8, 119, 0, 0, 12, 99, 0, 21, 13, 5, 12, 0, 12, 101, 0, 16, 14, 13, 12, 17, 12, 8, 14, 0, 8, 119, 0, 0, 13, 101, 0, 16, 14, 5, 13, 17, 13, 8, 14, 43, 8, 7, 9, 4, 10, 11, 12, 13, 11, 7, 3, 8, 5, 3, 7, 39, 2738, 45, 3, 0, 7, 5, 0, 45, 7, 4, 2, 0, 5, 7, 0, 5, 3, 5, 0, 5, 6, 0, 5, 4, 5, 0, 5, 82, 0, 8, 6, 0, 5, 22, 5, 4, 6, 40, 5, 3439, 2, 5, 114, 0, 0, 6, 115, 0, 8, 7, 5, 6, 0, 6, 93, 0, 8, 8, 0, 6, 43, 6, 0, 8, 1, 4, 0, 8, 127, 0, 16, 9, 6, 8, 0, 6, 93, 0, 8, 8, 0, 6, 43, 6, 0, 8, 1, 4, 0, 8, 83, 0, 21, 10, 6, 8, 0, 6, 127, 0, 16, 8, 10, 6, 43, 6, 5, 7, 2, 9, 8, 11, 5, 3, 6, 5, 3, 5, 5, 5, 4, 0, 5, 87, 0, 11, 6, 4, 5, 5, 4, 6, 39, 3330, 45, 3, 0, 5, 5, 0, 45, 5, 4, 2, 0, 5, 7, 0, 5, 3, 5, 0, 5, 6, 0, 5, 4, 5, 0, 5, 82, 0, 8, 6, 0, 5, 22, 5, 4, 6, 40, 5, 3572, 2, 5, 114, 0, 0, 6, 115, 0, 8, 7, 5, 6, 0, 6, 93, 0, 8, 8, 0, 6, 43, 6, 0, 8, 1, 4, 0, 8, 83, 0, 21, 9, 6, 8, 0, 6, 127, 0, 16, 8, 9, 6, 0, 6, 93, 0, 8, 9, 0, 6, 43, 6, 0, 9, 1, 4, 0, 9, 127, 0, 16, 10, 6, 9, 43, 6, 5, 7, 2, 8, 10, 11, 5, 3, 6, 5, 3, 5, 5, 5, 4, 0, 5, 87, 0, 11, 6, 4, 5, 5, 4, 6, 39, 3463, 45, 3, 0, 5, 5, 0, 45, 5, 4, 2, 2, 5, 8, 0, 0, 6, 82, 0, 8, 7, 0, 6, 0, 6, 98, 0, 20, 8, 7, 6, 42, 6, 5, 1, 8, 5, 3, 6, 0, 5, 6, 0, 5, 4, 5, 0, 5, 82, 0, 8, 6, 3, 5, 22, 5, 4, 6, 40, 5, 3656, 0, 5, 6, 0, 9, 3, 4, 5, 5, 5, 4, 0, 5, 87, 0, 11, 6, 4, 5, 5, 4, 6, 39, 3617, 0, 5, 6, 0, 5, 4, 5, 0, 5, 82, 0, 8, 6, 0, 5, 0, 5, 83, 0, 13, 7, 6, 5, 22, 5, 4, 7, 40, 5, 3769, 0, 5, 128, 0, 20, 6, 4, 5, 8, 5, 3, 6, 0, 7, 93, 0, 8, 8, 0, 7, 0, 7, 83, 0, 14, 9, 4, 7, 43, 7, 0, 8, 1, 9, 0, 8, 127, 0, 16, 9, 7, 8, 0, 7, 129, 0, 0, 8, 130, 0, 15, 10, 4, 8, 12, 8, 7, 10, 19, 7, 9, 8, 17, 8, 5, 7, 9, 3, 6, 8, 0, 5, 83, 0, 11, 6, 4, 5, 5, 4, 6, 39, 3663, 45, 3, 0, 5, 5, 0, 45, 5, 4, 2, 0, 5, 7, 0, 5, 3, 5, 0, 5, 6, 0, 5, 4, 5, 0, 5, 82, 0, 8, 6, 0, 5, 0, 5, 130, 0, 13, 7, 6, 5, 22, 5, 4, 7, 40, 5, 3894, 2, 5, 114, 0, 0, 6, 115, 0, 8, 7, 5, 6, 0, 6, 128, 0, 20, 8, 4, 6, 8, 6, 0, 8, 0, 8, 129, 0, 0, 9, 130, 0, 15, 10, 4, 9, 12, 9, 8, 10, 21, 8, 6, 9, 0, 6, 127, 0, 16, 9, 8, 6, 43, 6, 5, 7, 1, 9, 11, 5, 3, 6, 5, 3, 5, 0, 5, 83, 0, 11, 6, 4, 5, 5, 4, 6, 39, 3793, 45, 3, 0, 5, 5, 0, 45, 5, 4, 3, 21, 4, 0, 1, 0, 5, 130, 0, 12, 6, 5, 1, 19, 5, 0, 6, 17, 6, 4, 5, 45, 6, 0, 4, 5, 0, 45, 4, 4, 3, 21, 4, 0, 1, 45, 4, 0, 4, 5, 0, 45, 4, 4, 4, 16, 5, 0, 1, 35, 6, 0, 16, 7, 6, 2, 18, 6, 5, 7, 45, 6, 0, 5, 5, 0, 45, 5, 4, 4, 16, 5, 0, 1, 16, 6, 0, 2, 18, 7, 5, 6, 16, 5, 1, 2, 18, 6, 7, 5, 45, 6, 0, 5, 5, 0, 45, 5, 4, 2, 3, 3, 0, 0, 4, 98, 0, 42, 5, 3, 2, 0, 4, 3, 3, 0, 0, 4, 131, 0, 42, 6, 3, 2, 0, 4, 18, 3, 5, 6, 3, 4, 0, 0, 5, 132, 0, 42, 6, 4, 2, 0, 5, 18, 4, 3, 6, 45, 4, 0, 3, 5, 0, 45, 3, 4, 2, 3, 3, 0, 0, 4, 99, 0, 42, 5, 3, 2, 0, 4, 3, 3, 0, 0, 4, 133, 0, 42, 6, 3, 2, 0, 4, 18, 3, 5, 6, 3, 4, 0, 0, 5, 134, 0, 42, 6, 4, 2, 0, 5, 18, 4, 3, 6, 45, 4, 0, 3, 5, 0, 45, 3, 4, 2, 3, 3, 0, 0, 4, 126, 0, 42, 5, 3, 2, 0, 4, 3, 3, 0, 0, 4, 125, 0, 42, 6, 3, 2, 0, 4, 18, 3, 5, 6, 3, 4, 1, 0, 5, 100, 0, 42, 6, 4, 2, 0, 5, 18, 4, 3, 6, 45, 4, 0, 3, 5, 0, 45, 3, 4, 2, 3, 3, 0, 0, 4, 135, 0, 42, 5, 3, 2, 0, 4, 3, 3, 0, 0, 4, 136, 0, 42, 6, 3, 2, 0, 4, 18, 3, 5, 6, 3, 4, 1, 0, 5, 112, 0, 42, 6, 4, 2, 0, 5, 18, 4, 3, 6, 45, 4, 0, 3, 5, 0, 45, 3, 4, 2, 3, 3, 0, 0, 4, 137, 0, 42, 5, 3, 2, 0, 4, 3, 3, 0, 0, 4, 138, 0, 42, 6, 3, 2, 0, 4, 18, 3, 5, 6, 3, 4, 0, 0, 5, 139, 0, 42, 6, 4, 2, 0, 5, 18, 4, 3, 6, 45, 4, 0, 3, 5, 0, 45, 3, 4, 2, 3, 3, 0, 0, 4, 140, 0, 42, 5, 3, 2, 0, 4, 3, 3, 0, 0, 4, 125, 0, 42, 6, 3, 2, 0, 4, 18, 3, 5, 6, 3, 4, 0, 0, 5, 141, 0, 42, 6, 4, 2, 0, 5, 18, 4, 3, 6, 45, 4, 0, 3, 5, 0, 45, 3, 4, 2, 3, 3, 0, 0, 4, 87, 0, 42, 5, 3, 2, 0, 4, 3, 3, 0, 0, 4, 83, 0, 42, 6, 3, 2, 0, 4, 18, 3, 5, 6, 3, 4, 1, 0, 5, 126, 0, 42, 6, 4, 2, 0, 5, 18, 4, 3, 6, 45, 4, 0, 3, 5, 0, 45, 3, 4, 2, 3, 3, 0, 0, 4, 136, 0, 42, 5, 3, 2, 0, 4, 3, 3, 0, 0, 4, 142, 0, 42, 6, 3, 2, 0, 4, 18, 3, 5, 6, 3, 4, 1, 0, 5, 99, 0, 42, 6, 4, 2, 0, 5, 18, 4, 3, 6, 45, 4, 0, 3, 5, 0, 45, 3, 4, 3, 2, 18, 8, 0, 0, 19, 143, 0, 0, 20, 144, 0, 32, 21, 20, 0, 20, 145, 0, 0, 22, 146, 0, 32, 23, 22, 0, 22, 147, 0, 0, 24, 148, 0, 32, 25, 24, 0, 24, 149, 0, 0, 26, 150, 0, 44, 27, 18, 8, 19, 21, 20, 23, 22, 25, 24, 26, 5, 4, 27, 2, 18, 8, 0, 0, 19, 151, 0, 44, 20, 18, 1, 19, 5, 5, 20, 0, 6, 5, 0, 0, 7, 5, 0, 0, 8, 5, 0, 0, 9, 5, 0, 0, 10, 5, 0, 0, 11, 5, 0, 0, 12, 5, 0, 0, 13, 5, 0, 0, 14, 5, 0, 0, 15, 5, 0, 0, 16, 5, 0, 0, 17, 5, 0, 0, 18, 128, 0, 20, 19, 1, 18, 8, 18, 0, 19, 0, 20, 119, 0, 0, 21, 129, 0, 0, 22, 130, 0, 15, 23, 1, 22, 12, 22, 21, 23, 19, 21, 20, 22, 17, 20, 18, 21, 9, 0, 19, 20, 0, 18, 151, 0, 11, 19, 1, 18, 0, 18, 152, 0, 20, 20, 19, 18, 0, 18, 95, 0, 19, 19, 20, 18, 0, 18, 96, 0, 11, 20, 19, 18, 9, 0, 20, 1, 0, 18, 6, 0, 5, 14, 18, 0, 18, 82, 0, 8, 19, 0, 18, 22, 18, 14, 19, 40, 18, 5280, 0, 18, 6, 0, 8, 19, 4, 18, 5, 6, 19, 0, 18, 87, 0, 8, 19, 4, 18, 5, 7, 19, 0, 18, 98, 0, 8, 19, 4, 18, 5, 8, 19, 0, 18, 100, 0, 8, 19, 4, 18, 5, 9, 19, 0, 18, 95, 0, 8, 19, 4, 18, 5, 10, 19, 0, 18, 128, 0, 8, 19, 4, 18, 5, 11, 19, 0, 18, 99, 0, 8, 19, 4, 18, 5, 12, 19, 0, 18, 126, 0, 8, 19, 4, 18, 5, 13, 19, 0, 18, 6, 0, 5, 15, 18, 0, 18, 151, 0, 22, 19, 15, 18, 40, 19, 5067, 0, 18, 84, 0, 22, 19, 15, 18, 40, 19, 4816, 11, 18, 15, 14, 8, 19, 0, 18, 9, 5, 15, 19, 39, 4911, 3, 18, 0, 3, 19, 0, 3, 20, 0, 3, 21, 1, 0, 22, 98, 0, 12, 23, 15, 22, 8, 22, 5, 23, 42, 23, 21, 1, 22, 0, 21, 126, 0, 12, 22, 15, 21, 8, 21, 5, 22, 42, 22, 20, 2, 23, 21, 3, 20, 2, 0, 21, 96, 0, 12, 23, 15, 21, 8, 21, 5, 23, 42, 23, 20, 1, 21, 42, 20, 19, 2, 22, 23, 0, 19, 84, 0, 12, 21, 15, 19, 8, 19, 5, 21, 42, 21, 18, 2, 20, 19, 9, 5, 15, 21, 3, 18, 0, 3, 19, 0, 3, 20, 0, 3, 21, 0, 3, 22, 3, 42, 23, 22, 1, 10, 42, 22, 21, 2, 13, 23, 3, 21, 4, 42, 23, 21, 3, 10, 11, 12, 42, 21, 20, 2, 22, 23, 3, 20, 5, 8, 22, 20, 15, 42, 20, 19, 2, 21, 22, 8, 19, 5, 15, 42, 21, 18, 2, 20, 19, 5, 16, 21, 3, 18, 0, 3, 19, 6, 42, 20, 19, 1, 6, 3, 19, 7, 42, 21, 19, 3, 6, 7, 8, 42, 19, 18, 2, 20, 21, 5, 17, 19, 5, 13, 12, 5, 12, 11, 5, 11, 10, 3, 18, 0, 42, 19, 18, 2, 9, 16, 5, 10, 19, 5, 9, 8, 5, 8, 7, 5, 7, 6, 3, 18, 0, 42, 19, 18, 2, 16, 17, 5, 6, 19, 5, 18, 15, 0, 18, 87, 0, 11, 19, 15, 18, 5, 15, 19, 39, 4780, 0, 18, 6, 0, 3, 19, 0, 0, 20, 6, 0, 8, 21, 4, 20, 42, 20, 19, 2, 6, 21, 9, 4, 18, 20, 0, 18, 87, 0, 3, 19, 0, 0, 20, 87, 0, 8, 21, 4, 20, 42, 20, 19, 2, 7, 21, 9, 4, 18, 20, 0, 18, 98, 0, 3, 19, 0, 0, 20, 98, 0, 8, 21, 4, 20, 42, 20, 19, 2, 8, 21, 9, 4, 18, 20, 0, 18, 100, 0, 3, 19, 0, 0, 20, 100, 0, 8, 21, 4, 20, 42, 20, 19, 2, 9, 21, 9, 4, 18, 20, 0, 18, 95, 0, 3, 19, 0, 0, 20, 95, 0, 8, 21, 4, 20, 42, 20, 19, 2, 10, 21, 9, 4, 18, 20, 0, 18, 128, 0, 3, 19, 0, 0, 20, 128, 0, 8, 21, 4, 20, 42, 20, 19, 2, 11, 21, 9, 4, 18, 20, 0, 18, 99, 0, 3, 19, 0, 0, 20, 99, 0, 8, 21, 4, 20, 42, 20, 19, 2, 12, 21, 9, 4, 18, 20, 0, 18, 126, 0, 3, 19, 0, 0, 20, 126, 0, 8, 21, 4, 20, 42, 20, 19, 2, 13, 21, 9, 4, 18, 20, 0, 18, 84, 0, 11, 19, 14, 18, 5, 14, 19, 39, 4670, 45, 4, 0, 18, 5, 0, 45, 18, 4, 3, 0, 6, 120, 0, 16, 7, 0, 6, 0, 6, 120, 0, 16, 8, 1, 6, 11, 6, 7, 8, 5, 4, 6, 0, 6, 84, 0, 20, 7, 0, 6, 0, 6, 84, 0, 20, 8, 1, 6, 11, 6, 7, 8, 0, 7, 84, 0, 20, 8, 4, 7, 11, 7, 6, 8, 5, 5, 7, 0, 6, 84, 0, 19, 7, 5, 6, 0, 6, 120, 0, 16, 8, 4, 6, 17, 6, 7, 8, 45, 6, 0, 6, 5, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 6; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/oracle.json b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/oracle.json new file mode 100644 index 00000000..fa287da1 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/oracle.json @@ -0,0 +1,74 @@ +{ + "schemaVersion": "oracle.v1", + "cellId": "program-sha256", + "hostProfile": "minimal-window-v1", + "globals": {}, + "setup": { + "state": {}, + "aliases": [] + }, + "actions": [ + { + "op": "keys", + "root": "output", + "path": [], + "as": "keys" + }, + { + "op": "call", + "root": "output", + "path": [ + "hex" + ], + "args": [ + "Hello World!" + ], + "as": "hexHello" + }, + { + "op": "call", + "root": "output", + "path": [ + "hex" + ], + "args": [ + "Another SHA256" + ], + "as": "hexAnother" + } + ], + "timeoutMs": 2000, + "expected": { + "keys": [ + "hex", + "b64", + "any", + "hex_hmac", + "b64_hmac", + "any_hmac" + ], + "hexHello": "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", + "hexAnother": "846cf19f7bbbe6039a261bb17b693d27a77562843491bb9c053ba3b01921c10f" + }, + "expectedThrow": null, + "semanticTags": [ + "whole-program", + "nested-functions", + "closure", + "loops", + "bitwise", + "api" + ], + "hazards": [ + "Finite synchronous child-process oracle with SIGKILL timeout; no time, random, network, locale, async, or stack observables.", + "Tagged projection compares declared values only; function identity, object identity, and host-dependent stack data are excluded.", + "Post-evaluation observations use only the finite declarative actions recorded in the oracle; function values are never serialized." + ], + "exclusions": [ + "Compiler/runtime internals, debug comments, and opcode/IR coverage remain excluded until step 2." + ], + "comparison": { + "sourceRawStripped": "exact-normalized-observable", + "expectedKind": "normalized-value" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/source.js b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/source.js new file mode 100644 index 00000000..699e786c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/corpus/raw/program-sha256/source.js @@ -0,0 +1,459 @@ +// Modified by bryanchow for namespace control and higher compressibility +// See https://gist.github.com/1649353 for full revision history from original + +/* + * A JavaScript implementation of the Secure Hash Algorithm, SHA-256, as defined + * in FIPS 180-2 + * Version 2.2 Copyright Angel Marin, Paul Johnston 2000 - 2009. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for details. + * Also http://anmar.eu.org/projects/jssha2/ + */ + +var sha256 = (function () { + /* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ + var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ + var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ + + /* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ + function hex_sha256(s) { + return rstr2hex(rstr_sha256(str2rstr_utf8(s))); + } + function b64_sha256(s) { + return rstr2b64(rstr_sha256(str2rstr_utf8(s))); + } + function any_sha256(s, e) { + return rstr2any(rstr_sha256(str2rstr_utf8(s)), e); + } + function hex_hmac_sha256(k, d) { + return rstr2hex(rstr_hmac_sha256(str2rstr_utf8(k), str2rstr_utf8(d))); + } + function b64_hmac_sha256(k, d) { + return rstr2b64(rstr_hmac_sha256(str2rstr_utf8(k), str2rstr_utf8(d))); + } + function any_hmac_sha256(k, d, e) { + return rstr2any(rstr_hmac_sha256(str2rstr_utf8(k), str2rstr_utf8(d)), e); + } + + /* + * Perform a simple self-test to see if the VM is working + */ + function sha256_vm_test() { + return ( + hex_sha256("abc").toLowerCase() == + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); + } + + /* + * Calculate the sha256 of a raw string + */ + function rstr_sha256(s) { + return binb2rstr(binb_sha256(rstr2binb(s), s.length * 8)); + } + + /* + * Calculate the HMAC-sha256 of a key and some data (raw strings) + */ + function rstr_hmac_sha256(key, data) { + var bkey = rstr2binb(key); + if (bkey.length > 16) bkey = binb_sha256(bkey, key.length * 8); + + var ipad = Array(16), + opad = Array(16); + for (var i = 0; i < 16; i++) { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5c5c5c5c; + } + + var hash = binb_sha256(ipad.concat(rstr2binb(data)), 512 + data.length * 8); + return binb2rstr(binb_sha256(opad.concat(hash), 512 + 256)); + } + + /* + * Convert a raw string to a hex string + */ + function rstr2hex(input) { + try { + hexcase; + } catch (e) { + hexcase = 0; + } + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var output = ""; + var x; + for (var i = 0; i < input.length; i++) { + x = input.charCodeAt(i); + output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f); + } + return output; + } + + /* + * Convert a raw string to a base-64 string + */ + function rstr2b64(input) { + try { + b64pad; + } catch (e) { + b64pad = ""; + } + var tab = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var output = ""; + var len = input.length; + for (var i = 0; i < len; i += 3) { + var triplet = + (input.charCodeAt(i) << 16) | + (i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) | + (i + 2 < len ? input.charCodeAt(i + 2) : 0); + for (var j = 0; j < 4; j++) { + if (i * 8 + j * 6 > input.length * 8) output += b64pad; + else output += tab.charAt((triplet >>> (6 * (3 - j))) & 0x3f); + } + } + return output; + } + + /* + * Convert a raw string to an arbitrary string encoding + */ + function rstr2any(input, encoding) { + var divisor = encoding.length; + var remainders = Array(); + var i, q, x, quotient; + + /* Convert to an array of 16-bit big-endian values, forming the dividend */ + var dividend = Array(Math.ceil(input.length / 2)); + for (i = 0; i < dividend.length; i++) { + dividend[i] = + (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); + } + + /* + * Repeatedly perform a long division. The binary array forms the dividend, + * the length of the encoding is the divisor. Once computed, the quotient + * forms the dividend for the next step. We stop when the dividend is zero. + * All remainders are stored for later use. + */ + while (dividend.length > 0) { + quotient = Array(); + x = 0; + for (i = 0; i < dividend.length; i++) { + x = (x << 16) + dividend[i]; + q = Math.floor(x / divisor); + x -= q * divisor; + if (quotient.length > 0 || q > 0) quotient[quotient.length] = q; + } + remainders[remainders.length] = x; + dividend = quotient; + } + + /* Convert the remainders to the output string */ + var output = ""; + for (i = remainders.length - 1; i >= 0; i--) + output += encoding.charAt(remainders[i]); + + /* Append leading zero equivalents */ + var full_length = Math.ceil( + (input.length * 8) / (Math.log(encoding.length) / Math.log(2)), + ); + for (i = output.length; i < full_length; i++) output = encoding[0] + output; + + return output; + } + + /* + * Encode a string as utf-8. + * For efficiency, this assumes the input is valid utf-16. + */ + function str2rstr_utf8(input) { + var output = ""; + var i = -1; + var x, y; + + while (++i < input.length) { + /* Decode utf-16 surrogate pairs */ + x = input.charCodeAt(i); + y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; + if (0xd800 <= x && x <= 0xdbff && 0xdc00 <= y && y <= 0xdfff) { + x = 0x10000 + ((x & 0x03ff) << 10) + (y & 0x03ff); + i++; + } + + /* Encode output as utf-8 */ + if (x <= 0x7f) output += String.fromCharCode(x); + else if (x <= 0x7ff) + output += String.fromCharCode( + 0xc0 | ((x >>> 6) & 0x1f), + 0x80 | (x & 0x3f), + ); + else if (x <= 0xffff) + output += String.fromCharCode( + 0xe0 | ((x >>> 12) & 0x0f), + 0x80 | ((x >>> 6) & 0x3f), + 0x80 | (x & 0x3f), + ); + else if (x <= 0x1fffff) + output += String.fromCharCode( + 0xf0 | ((x >>> 18) & 0x07), + 0x80 | ((x >>> 12) & 0x3f), + 0x80 | ((x >>> 6) & 0x3f), + 0x80 | (x & 0x3f), + ); + } + return output; + } + + /* + * Encode a string as utf-16 + */ + function str2rstr_utf16le(input) { + var output = ""; + for (var i = 0; i < input.length; i++) + output += String.fromCharCode( + input.charCodeAt(i) & 0xff, + (input.charCodeAt(i) >>> 8) & 0xff, + ); + return output; + } + + function str2rstr_utf16be(input) { + var output = ""; + for (var i = 0; i < input.length; i++) + output += String.fromCharCode( + (input.charCodeAt(i) >>> 8) & 0xff, + input.charCodeAt(i) & 0xff, + ); + return output; + } + + /* + * Convert a raw string to an array of big-endian words + * Characters >255 have their high-byte silently ignored. + */ + function rstr2binb(input) { + var output = Array(input.length >> 2); + for (var i = 0; i < output.length; i++) output[i] = 0; + for (var i = 0; i < input.length * 8; i += 8) + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (24 - (i % 32)); + return output; + } + + /* + * Convert an array of big-endian words to a string + */ + function binb2rstr(input) { + var output = ""; + for (var i = 0; i < input.length * 32; i += 8) + output += String.fromCharCode((input[i >> 5] >>> (24 - (i % 32))) & 0xff); + return output; + } + + /* + * Main sha256 function, with its support functions + */ + function sha256_S(X, n) { + return (X >>> n) | (X << (32 - n)); + } + function sha256_R(X, n) { + return X >>> n; + } + function sha256_Ch(x, y, z) { + return (x & y) ^ (~x & z); + } + function sha256_Maj(x, y, z) { + return (x & y) ^ (x & z) ^ (y & z); + } + function sha256_Sigma0256(x) { + return sha256_S(x, 2) ^ sha256_S(x, 13) ^ sha256_S(x, 22); + } + function sha256_Sigma1256(x) { + return sha256_S(x, 6) ^ sha256_S(x, 11) ^ sha256_S(x, 25); + } + function sha256_Gamma0256(x) { + return sha256_S(x, 7) ^ sha256_S(x, 18) ^ sha256_R(x, 3); + } + function sha256_Gamma1256(x) { + return sha256_S(x, 17) ^ sha256_S(x, 19) ^ sha256_R(x, 10); + } + function sha256_Sigma0512(x) { + return sha256_S(x, 28) ^ sha256_S(x, 34) ^ sha256_S(x, 39); + } + function sha256_Sigma1512(x) { + return sha256_S(x, 14) ^ sha256_S(x, 18) ^ sha256_S(x, 41); + } + function sha256_Gamma0512(x) { + return sha256_S(x, 1) ^ sha256_S(x, 8) ^ sha256_R(x, 7); + } + function sha256_Gamma1512(x) { + return sha256_S(x, 19) ^ sha256_S(x, 61) ^ sha256_R(x, 6); + } + + var sha256_K = new Array( + 1116352408, + 1899447441, + -1245643825, + -373957723, + 961987163, + 1508970993, + -1841331548, + -1424204075, + -670586216, + 310598401, + 607225278, + 1426881987, + 1925078388, + -2132889090, + -1680079193, + -1046744716, + -459576895, + -272742522, + 264347078, + 604807628, + 770255983, + 1249150122, + 1555081692, + 1996064986, + -1740746414, + -1473132947, + -1341970488, + -1084653625, + -958395405, + -710438585, + 113926993, + 338241895, + 666307205, + 773529912, + 1294757372, + 1396182291, + 1695183700, + 1986661051, + -2117940946, + -1838011259, + -1564481375, + -1474664885, + -1035236496, + -949202525, + -778901479, + -694614492, + -200395387, + 275423344, + 430227734, + 506948616, + 659060556, + 883997877, + 958139571, + 1322822218, + 1537002063, + 1747873779, + 1955562222, + 2024104815, + -2067236844, + -1933114872, + -1866530822, + -1538233109, + -1090935817, + -965641998, + ); + + function binb_sha256(m, l) { + var HASH = new Array( + 1779033703, + -1150833019, + 1013904242, + -1521486534, + 1359893119, + -1694144372, + 528734635, + 1541459225, + ); + var W = new Array(64); + var a, b, c, d, e, f, g, h; + var i, j, T1, T2; + + /* append padding */ + m[l >> 5] |= 0x80 << (24 - (l % 32)); + m[(((l + 64) >> 9) << 4) + 15] = l; + + for (i = 0; i < m.length; i += 16) { + a = HASH[0]; + b = HASH[1]; + c = HASH[2]; + d = HASH[3]; + e = HASH[4]; + f = HASH[5]; + g = HASH[6]; + h = HASH[7]; + + for (j = 0; j < 64; j++) { + if (j < 16) W[j] = m[j + i]; + else + W[j] = safe_add( + safe_add( + safe_add(sha256_Gamma1256(W[j - 2]), W[j - 7]), + sha256_Gamma0256(W[j - 15]), + ), + W[j - 16], + ); + + T1 = safe_add( + safe_add( + safe_add(safe_add(h, sha256_Sigma1256(e)), sha256_Ch(e, f, g)), + sha256_K[j], + ), + W[j], + ); + T2 = safe_add(sha256_Sigma0256(a), sha256_Maj(a, b, c)); + h = g; + g = f; + f = e; + e = safe_add(d, T1); + d = c; + c = b; + b = a; + a = safe_add(T1, T2); + } + + HASH[0] = safe_add(a, HASH[0]); + HASH[1] = safe_add(b, HASH[1]); + HASH[2] = safe_add(c, HASH[2]); + HASH[3] = safe_add(d, HASH[3]); + HASH[4] = safe_add(e, HASH[4]); + HASH[5] = safe_add(f, HASH[5]); + HASH[6] = safe_add(g, HASH[6]); + HASH[7] = safe_add(h, HASH[7]); + } + return HASH; + } + + function safe_add(x, y) { + var lsw = (x & 0xffff) + (y & 0xffff); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xffff); + } + + return { + hex: hex_sha256, + b64: b64_hmac_sha256, + any: any_hmac_sha256, + hex_hmac: hex_hmac_sha256, + b64_hmac: b64_hmac_sha256, + any_hmac: any_hmac_sha256, + }; +})(); + +// test-utils.js only defines 'window' and can't use 'module' +if (typeof module !== "undefined") { + module.exports = sha256; +} +if (typeof window !== "undefined") { + window.TEST_OUTPUT = sha256; +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-169.js b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-169.js new file mode 100644 index 00000000..f4224cf0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-169.js @@ -0,0 +1,17 @@ +function c(a,f,h){this.d=a;this.A=h;this.h=f;this.l=null;this.p=[];this.c=1;this.g=0}var B=new WeakMap;function fa(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})}function C(a,f){this.f="x";this.p=a;this.m=f;this.u=!1;this.o=void 0}function D(a,f,h){this.E=h;this.s=a;this.i=[];this.prototype={};this.r=f}C.prototype.e=function(a,f,h){this.u?this.o=h:this.p[this.m]=h};C.prototype.Q=function(){return this.u?this.o:this.p[this.m]}; +c.prototype.k=function(a,f,h){a=h??J(this);f=f??J(this);a=this.A[a];if(!f)return a;if(typeof a==="number")return a^f;if(typeof a!=="string")return a;a=fa(a);h="";for(var b=0;b>>13)&65535);return h};function ha(a,f,h){f=a.p[f+7]+h;h=a.l||(a.l=[]);return h[f]||(h[f]=new C(a.p,f))}function J(a){return a.d[a.p[a.g+0]++]} +function K(a,f,h,b,l,g){for(var m=l.s,d=a.p,e=a.c,u=8+m.z,x=e+u;d.length=a.d.length)break;b[g+0]=m+1;m=a.d[m];try{var d=b[g+7];switch(m){case 0:var e=J(a);b[d+e]=a.k();break;case 1:e=J(a);b[d+e]=J(a);break;case 2:e=J(a);var u=a.k();if(!(u in a.h))throw new ReferenceError(`${u} is not defined`);b[d+e]=a.h[u];break;case 3:e=J(a);b[d+e]=b[g+4].i[J(a)].Q(void 0,void 0,[],14);break;case 4:e=J(a);b[d+e]=b[g+3];break;case 5:e=J(a);b[d+e]=b[d+J(a)];break;case 6:a.h[a.k()]=b[d+J(a)];break;case 7:var x= +J(a);b[g+4].i[x].e(21,void 0,b[d+J(a)]);break;case 8:e=J(a);var n=b[d+J(a)],p=b[d+J(a)];b[d+e]=n[p];break;case 9:n=b[d+J(a)];p=b[d+J(a)];var y=b[d+J(a)];Reflect.set(n,p,y);break;case 10:e=J(a);n=b[d+J(a)];p=b[d+J(a)];b[d+e]=delete n[p];break;case 11:e=J(a);var k=b[d+J(a)];b[d+e]=k+b[d+J(a)];break;case 12:e=J(a);k=b[d+J(a)];b[d+e]=k-b[d+J(a)];break;case 13:e=J(a);k=b[d+J(a)];b[d+e]=k*b[d+J(a)];break;case 14:e=J(a);k=b[d+J(a)];b[d+e]=k/b[d+J(a)];break;case 15:e=J(a);k=b[d+J(a)];b[d+e]=k%b[d+J(a)];break; +case 60:e=J(a);k=b[d+J(a)];b[d+e]=Math.pow(k,b[d+J(a)]);break;case 16:e=J(a);k=b[d+J(a)];b[d+e]=k&b[d+J(a)];break;case 17:e=J(a);k=b[d+J(a)];b[d+e]=k|b[d+J(a)];break;case 18:e=J(a);k=b[d+J(a)];b[d+e]=k^b[d+J(a)];break;case 19:e=J(a);k=b[d+J(a)];b[d+e]=k<>b[d+J(a)];break;case 21:e=J(a);k=b[d+J(a)];b[d+e]=k>>>b[d+J(a)];break;case 22:e=J(a);k=b[d+J(a)];b[d+e]=kb[d+J(a)];break;case 24:e=J(a);k=b[d+ +J(a)];b[d+e]=k<=b[d+J(a)];break;case 25:e=J(a);k=b[d+J(a)];b[d+e]=k>=b[d+J(a)];break;case 26:e=J(a);k=b[d+J(a)];b[d+e]=k===b[d+J(a)];break;case 27:e=J(a);k=b[d+J(a)];b[d+e]=k!==b[d+J(a)];break;case 28:e=J(a);k=b[d+J(a)];b[d+e]=k==b[d+J(a)];break;case 29:e=J(a);k=b[d+J(a)];b[d+e]=k!=b[d+J(a)];break;case 30:e=J(a);k=b[d+J(a)];b[d+e]=k in b[d+J(a)];break;case 31:e=J(a);n=b[d+J(a)];b[d+e]=n instanceof b[d+J(a)];break;case 32:e=J(a);b[d+e]=-b[d+J(a)];break;case 33:e=J(a);b[d+e]=+b[d+J(a)];break;case 34:e= +J(a);b[d+e]=!b[d+J(a)];break;case 35:e=J(a);b[d+e]=~b[d+J(a)];break;case 36:e=J(a);b[d+e]=typeof b[d+J(a)];break;case 37:e=J(a);J(a);b[d+e]=void 0;break;case 38:e=J(a);var U=a.k();y=Object.prototype.hasOwnProperty.call(a.h,U)?a.h[U]:void 0;b[d+e]=typeof y;break;case 39:b[g+0]=J(a);break;case 40:var L=J(a),M=J(a);b[d+L]||(b[g+0]=M);break;case 41:L=J(a);M=J(a);b[d+L]&&(b[g+0]=M);break;case 42:e=J(a);var q=b[d+J(a)],r=J(a);if(r===65535)f=b[d+J(a)];else for(f=Array(r),l=0;l>1):0]=E;break;case 46:throw b[d+J(a)];case 47:e=J(a);var ja=J(a),ka=J(a),la=J(a),X=J(a),ma=J(a),O=Array(X);for(l=0;l=H.D.length?b[g+0]=sa:b[d+e]=H.D[H.v++];break;case 54:var v=b[g+5];v||(b[g+5]=v=[]);v.push({a:J(a),B:J(a)});break;case 55:b[g+5].pop();break;case 59:(v=b[g+5])||(b[g+5]=v=[]);v.push({b:J(a),J:J(a),I:J(a),H:J(a)});break;case 56:var da=J(a),ea=J(a),ta=J(a),I=J(a)^da|0;for(g=ea;g>>13)>>>0;break;case 58:b[g+0]=b[d+J(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+" at pc "+(b[g+0]- +1));}}catch(z){g=0;for(m=a.g;m;){var w=b[m+5];if(w&&w.length>0){g=m;break}R(a,m);a.c=m;m=b[m+1];a.g=m}if(!g)throw z;m=b[g+5].pop();w=b[g+7];m.B!==void 0?(b[w+m.B]=z,b[g+0]=m.a):(b[w+m.J]=m.H,b[w+m.I]=z,b[g+0]=m.b);a.c=g+b[g+6];a.g=g}if(!a.g)return a.p[0]}}var T=globalThis;typeof window!=="undefined"&&(T.window=window,T.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(T.module=module,T.exports=typeof exports!=="undefined"?exports:void 0); +S(new c([4,1,47,3,69,1,9,0,0,5,2,3,2,3,0,0,0,4,1,0,0,5,2,0,32,6,5,42,5,2,1,6,0,6,3,0,42,7,2,1,6,2,6,0,0,0,8,4,0,8,9,6,8,48,6,3,5,7,9,9,3,4,6,0,3,5,0,45,3,4,2,59,149,4,5,167,54,126,3,0,6,6,0,22,7,0,6,40,7,105,2,6,7,0,0,7,8,0,44,8,6,1,7,46,8,0,6,2,0,11,7,0,6,55,5,5,7,1,4,169,55,39,149,55,39,143,0,6,9,0,8,7,3,6,5,5,7,1,4,171,55,39,149,55,1,4,173,39,149,2,6,0,0,0,7,4,0,0,8,10,0,9,6,7,8,58,4,46,5,45,5,45,5,0,6,5,0,45,6],T,["window","TEST_OUTPUT",1,4,"finished",void 0,0,"Error","negative","message",!0]), +null,new D({j:0,z:10,C:0},null,[])); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-170.js b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-170.js new file mode 100644 index 00000000..a2b5e02e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-170.js @@ -0,0 +1,19 @@ +function b(a,f,e,l,k){this.h=l;this.C=a;this.E=e;this.g=k;this.t=null;this.f=[];this.z=f;this.j=1;this.s=0}var A=new WeakMap;function B(a){this.G=a;this.i=[];this.prototype={}}function C(a,f){this.f=f;this.o=a;this.u=!1;this.q=void 0}function da(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})} +b.prototype.w=function(a){var f=this.t;if(f){var e=this.f,l=a+e[a+6];for(a=e[a+7];a>>13)&65535);return l}function ea(a,f){a.u?a.q=f:a.f[a.o]=f}function fa(a,f,e){a.x="q";f=a.f[f+7]+e;e=a.t||(a.t=[]);return e[f]||(e[f]=new C(f,a.f))} +b.prototype.e=function(a,f,e,l,k,p,n){var g;this.n=e;for(D(this,0,l,n,f);;){e=this.s;a=this.f;k=a[e+0];if(k>=this.h.length)break;a[e+0]=k+1;k=this.h[k];try{var c=a[e+7];switch(k){case 0:var d=E(this);a[c+d]=Q(this);break;case 1:d=E(this);a[c+d]=E(this);break;case 2:d=E(this);var q=Q(this);if(!(q in this.g))throw new ReferenceError(`${q} is not defined`);a[c+d]=this.g[q];break;case 3:d=E(this);a[c+d]=a[e+4].i[E(this)].r(void 0,null,"x");break;case 4:d=E(this);a[c+d]=a[e+3];break;case 5:d=E(this);a[c+ +d]=a[c+E(this)];break;case 6:this.g[Q(this)]=a[c+E(this)];break;case 7:var ha=E(this);ea(a[e+4].i[ha],a[c+E(this)]);break;case 8:d=E(this);var m=a[c+E(this)],r=a[c+E(this)];a[c+d]=m[r];break;case 9:m=a[c+E(this)];r=a[c+E(this)];var x=a[c+E(this)];Reflect.set(m,r,x);break;case 10:d=E(this);m=a[c+E(this)];r=a[c+E(this)];a[c+d]=delete m[r];break;case 11:d=E(this);var h=a[c+E(this)];a[c+d]=h+a[c+E(this)];break;case 12:d=E(this);h=a[c+E(this)];a[c+d]=h-a[c+E(this)];break;case 13:d=E(this);h=a[c+E(this)]; +a[c+d]=h*a[c+E(this)];break;case 14:d=E(this);h=a[c+E(this)];a[c+d]=h/a[c+E(this)];break;case 15:d=E(this);h=a[c+E(this)];a[c+d]=h%a[c+E(this)];break;case 60:d=E(this);h=a[c+E(this)];a[c+d]=Math.pow(h,a[c+E(this)]);break;case 16:d=E(this);h=a[c+E(this)];a[c+d]=h&a[c+E(this)];break;case 17:d=E(this);h=a[c+E(this)];a[c+d]=h|a[c+E(this)];break;case 18:d=E(this);h=a[c+E(this)];a[c+d]=h^a[c+E(this)];break;case 19:d=E(this);h=a[c+E(this)];a[c+d]=h<>a[c+E(this)];break;case 21:d=E(this);h=a[c+E(this)];a[c+d]=h>>>a[c+E(this)];break;case 22:d=E(this);h=a[c+E(this)];a[c+d]=ha[c+E(this)];break;case 24:d=E(this);h=a[c+E(this)];a[c+d]=h<=a[c+E(this)];break;case 25:d=E(this);h=a[c+E(this)];a[c+d]=h>=a[c+E(this)];break;case 26:d=E(this);h=a[c+E(this)];a[c+d]=h===a[c+E(this)];break;case 27:d=E(this);h=a[c+E(this)];a[c+d]=h!==a[c+E(this)];break;case 28:d=E(this);h=a[c+E(this)];a[c+d]=h== +a[c+E(this)];break;case 29:d=E(this);h=a[c+E(this)];a[c+d]=h!=a[c+E(this)];break;case 30:d=E(this);h=a[c+E(this)];a[c+d]=h in a[c+E(this)];break;case 31:d=E(this);m=a[c+E(this)];a[c+d]=m instanceof a[c+E(this)];break;case 32:d=E(this);a[c+d]=-a[c+E(this)];break;case 33:d=E(this);a[c+d]=+a[c+E(this)];break;case 34:d=E(this);a[c+d]=!a[c+E(this)];break;case 35:d=E(this);a[c+d]=~a[c+E(this)];break;case 36:d=E(this);a[c+d]=typeof a[c+E(this)];break;case 37:d=E(this);E(this);a[c+d]=void 0;break;case 38:d= +E(this);var S=Q(this);x=Object.prototype.hasOwnProperty.call(this.g,S)?this.g[S]:void 0;a[c+d]=typeof x;break;case 39:a[e+0]=E(this);break;case 40:var K=E(this),L=E(this);a[c+K]||(a[e+0]=L);break;case 41:K=E(this);L=E(this);a[c+K]&&(a[e+0]=L);break;case 42:d=E(this);var t=a[c+E(this)],u=E(this);if(u===65535)f=a[c+E(this)];else for(f=Array(u),g=0;g>1):0]=F;break;case 46:throw a[c+E(this)];case 47:d=E(this);var ja=E(this),ka=E(this),la=E(this),V=E(this),ma=E(this),N=Array(V);for(g=0;g=I.a.length?a[e+0]=sa:a[c+d]=I.a[I.B++];break;case 54:var w=a[e+5];w||(a[e+5]=w=[]);w.push({K:E(this),A:E(this)});break;case 55:a[e+5].pop();break;case 59:(w=a[e+5])||(a[e+5]=w=[]);w.push({J:E(this),c:E(this),F:E(this),H:E(this)});break;case 56:var ba=E(this),ca=E(this),ta=E(this),J=E(this)^ba|0;for(e=ca;e>>13)>>>0;break;case 58:a[e+0]=a[c+E(this)];break;case 57:debugger; +break;default:throw Error("Unknown opcode: "+k+" at pc "+(a[e+0]-1));}}catch(y){e=0;for(k=this.s;k;){if((p=a[k+5])&&p.length>0){e=k;break}this.w(k,void 0,17);this.j=k;this.s=k=a[k+1]}if(!e)throw y;k=a[e+5].pop();p=a[e+7];k.A!==void 0?(a[p+k.A]=y,a[e+0]=k.K):(a[p+k.c]=k.H,a[p+k.F]=y,a[e+0]=k.J);this.j=e+a[e+6];this.s=e}if(!this.s)return this.f[0]}};var R=globalThis;typeof window!=="undefined"&&(R.window=window,R.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(R.module=module,R.exports=typeof exports!=="undefined"?exports:void 0); +(new b(["window","TEST_OUTPUT",1,4,"finished",void 0,0,"Error","negative","message",!0],null,"q",[4,1,47,3,69,1,9,0,0,5,2,3,2,3,0,0,0,4,1,0,0,5,2,0,32,6,5,42,5,2,1,6,0,6,3,0,42,7,2,1,6,2,6,0,0,0,8,4,0,8,9,6,8,48,6,3,5,7,9,9,3,4,6,0,3,5,0,45,3,4,2,59,149,4,5,167,54,126,3,0,6,6,0,22,7,0,6,40,7,105,2,6,7,0,0,7,8,0,44,8,6,1,7,46,8,0,6,2,0,11,7,0,6,55,5,5,7,1,4,169,55,39,149,55,39,143,0,6,9,0,8,7,3,6,5,5,7,1,4,171,55,39,149,55,1,4,173,39,149,2,6,0,0,0,7,4,0,0,8,10,0,9,6,7,8,58,4,46,5,45,5,45,5,0,6,5,0, +45,6],R)).e("k",null,[],new B({m:0,l:10,v:0}),{},"k",void 0); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-171.js b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-171.js new file mode 100644 index 00000000..83c811f0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/encoded-171.js @@ -0,0 +1,17 @@ +function c(a,b,h,f){this.i=a;this.a=b;this.C=f;this.p=h;this.A=null;this.g=[];this.l=1;this.h=0}var B=new WeakMap;function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}function fa(a,b){this.g=a;this.v=b;this.u=!1;this.B=void 0}function C(a,b){this.e=b;this.j=[];this.prototype={};this.d=a} +c.prototype.c=function(a,b,h,f,l,m,k){f=m.e;l=this.g;for(var d=this.l,e=8+f.m,u=d+e;l.length>>13)&65535);return b}function ha(a,b,h){b=a.g[h+7]+b;h=a.A||(a.A=[]);return h[b]||(h[b]=new fa(a.g,b))}function ia(a){return a.u?a.B:a.g[a.v]}function Q(a,b){var h=a.A;if(h){var f=a.g;a=b+f[b+6];for(b=f[b+7];b=a.i.length)break;b[f+0]=m+1;m=a.i[m];try{var d=b[f+7];switch(m){case 0:var e=J(a);b[d+e]=D(a);break;case 1:e=J(a);b[d+e]=J(a);break;case 2:e=J(a);var u=D(a);if(!(u in a.p))throw new ReferenceError(`${u} is not defined`);b[d+e]=a.p[u];break;case 3:e=J(a);b[d+e]=ia(b[f+4].j[J(a)]);break;case 4:e=J(a);b[d+e]=b[f+3];break;case 5:e=J(a);b[d+e]=b[d+J(a)];break;case 6:a.p[D(a)]=b[d+J(a)];break;case 7:var q= +J(a);ja(b[f+4].j[q],b[d+J(a)]);break;case 8:e=J(a);var n=b[d+J(a)],p=b[d+J(a)];b[d+e]=n[p];break;case 9:n=b[d+J(a)];p=b[d+J(a)];var y=b[d+J(a)];Reflect.set(n,p,y);break;case 10:e=J(a);n=b[d+J(a)];p=b[d+J(a)];b[d+e]=delete n[p];break;case 11:e=J(a);var g=b[d+J(a)];b[d+e]=g+b[d+J(a)];break;case 12:e=J(a);g=b[d+J(a)];b[d+e]=g-b[d+J(a)];break;case 13:e=J(a);g=b[d+J(a)];b[d+e]=g*b[d+J(a)];break;case 14:e=J(a);g=b[d+J(a)];b[d+e]=g/b[d+J(a)];break;case 15:e=J(a);g=b[d+J(a)];b[d+e]=g%b[d+J(a)];break;case 60:e= +J(a);g=b[d+J(a)];b[d+e]=Math.pow(g,b[d+J(a)]);break;case 16:e=J(a);g=b[d+J(a)];b[d+e]=g&b[d+J(a)];break;case 17:e=J(a);g=b[d+J(a)];b[d+e]=g|b[d+J(a)];break;case 18:e=J(a);g=b[d+J(a)];b[d+e]=g^b[d+J(a)];break;case 19:e=J(a);g=b[d+J(a)];b[d+e]=g<>b[d+J(a)];break;case 21:e=J(a);g=b[d+J(a)];b[d+e]=g>>>b[d+J(a)];break;case 22:e=J(a);g=b[d+J(a)];b[d+e]=gb[d+J(a)];break;case 24:e=J(a);g=b[d+J(a)];b[d+ +e]=g<=b[d+J(a)];break;case 25:e=J(a);g=b[d+J(a)];b[d+e]=g>=b[d+J(a)];break;case 26:e=J(a);g=b[d+J(a)];b[d+e]=g===b[d+J(a)];break;case 27:e=J(a);g=b[d+J(a)];b[d+e]=g!==b[d+J(a)];break;case 28:e=J(a);g=b[d+J(a)];b[d+e]=g==b[d+J(a)];break;case 29:e=J(a);g=b[d+J(a)];b[d+e]=g!=b[d+J(a)];break;case 30:e=J(a);g=b[d+J(a)];b[d+e]=g in b[d+J(a)];break;case 31:e=J(a);n=b[d+J(a)];b[d+e]=n instanceof b[d+J(a)];break;case 32:e=J(a);b[d+e]=-b[d+J(a)];break;case 33:e=J(a);b[d+e]=+b[d+J(a)];break;case 34:e=J(a);b[d+ +e]=!b[d+J(a)];break;case 35:e=J(a);b[d+e]=~b[d+J(a)];break;case 36:e=J(a);b[d+e]=typeof b[d+J(a)];break;case 37:e=J(a);J(a);b[d+e]=void 0;break;case 38:e=J(a);var T=D(a);y=Object.prototype.hasOwnProperty.call(a.p,T)?a.p[T]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=J(a);break;case 40:var K=J(a),L=J(a);b[d+K]||(b[f+0]=L);break;case 41:K=J(a);L=J(a);b[d+K]&&(b[f+0]=L);break;case 42:e=J(a);var r=b[d+J(a)],t=J(a);if(t===65535)l=b[d+J(a)];else for(l=Array(t),k=0;k>1):0]=E;break;case 46:throw b[d+J(a)];case 47:e=J(a);var la=J(a),ma=J(a),na=J(a),W=J(a),oa=J(a),N=Array(W);for(k=0;k=H.k.length?b[f+0]=ua:b[d+e]=H.k[H.b++];break;case 54:var w=b[f+5];w||(b[f+5]=w=[]);w.push({F:J(a),r:J(a)});break;case 55:b[f+5].pop();break;case 59:(w=b[f+5])||(b[f+5]=w=[]);w.push({q:J(a),Q:J(a),x:J(a),z:J(a)});break;case 56:var ca=J(a),da=J(a),va=J(a),I=J(a)^ca|0;for(f=da;f>>13)>>>0;break;case 58:b[f+0]=b[d+J(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+ +" at pc "+(b[f+0]-1));}}catch(z){f=0;for(m=a.h;m;){var x=b[m+5];if(x&&x.length>0){f=m;break}Q(a,m);a.l=m;m=b[m+1];a.h=m}if(!f)throw z;m=b[f+5].pop();x=b[f+7];m.r!==void 0?(b[x+m.r]=z,b[f+0]=m.F):(b[x+m.Q]=m.z,b[x+m.x]=z,b[f+0]=m.q);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new c([4,1,47,3,69,1,9,0,0,5,2,3,2,3,0,0,0,4,1,0,0,5,2,0,32,6,5,42,5,2,1,6,0,6,3,0,42,7,2,1,6,2,6,0,0,0,8,4,0,8,9,6,8,48,6,3,5,7,9,9,3,4,6,0,3,5,0,45,3,4,2,59,149,4,5,167,54,126,3,0,6,6,0,22,7,0,6,40,7,105,2,6,7,0,0,7,8,0,44,8,6,1,7,46,8,0,6,2,0,11,7,0,6,55,5,5,7,1,4,169,55,39,149,55,39,143,0,6,9,0,8,7,3,6,5,5,7,1,4,171,55,39,149,55,1,4,173,39,149,2,6,0,0,0,7,4,0,0,8,10,0,9,6,7,8,58,4,46,5,45,5,45,5,0,6,5,0,45,6],54,S,["window","TEST_OUTPUT",1,4,"finished",void 0,0,"Error","negative","message", +!0]),void 0,new C("y",{o:0,m:10,D:0}),"y",null,72); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/outer-layers/compact.encoded.js b/test/vm/jsconfuser-vm/fixtures/outer-layers/compact.encoded.js new file mode 100644 index 00000000..4f8f9d68 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/outer-layers/compact.encoded.js @@ -0,0 +1 @@ +var CONSTANTS=["window","TEST_OUTPUT",4,"Math","max",2,7,100,undefined,"value",0,1];var BYTECODE=[63815,1,3126,4,90,1,4,0,0,51108,2,4,3126,4,106,1,8,1,0,1,2,51108,3,4,9745,4,0,0,58675,5,1,0,58675,6,2,0,31339,7,3,1,6,9745,6,3,0,58675,8,4,0,9119,9,6,8,58675,8,5,0,58675,10,6,0,49178,11,6,9,2,8,10,58675,6,7,0,3343,8,11,6,45047,6,7,8,21956,4,5,6,58675,4,8,0,10097,4,63815,2,58675,3,9,0,21956,2,3,0,58675,3,8,0,10097,3,63815,2,62699,5,0,58675,6,10,0,45538,7,5,1,6,51108,3,7,58675,5,10,0,51108,4,5,6858,5,4,0,62628,5,169,58675,5,9,0,9119,6,3,5,45047,7,6,4,21956,3,5,7,51108,5,4,58675,5,11,0,45047,6,4,5,51108,4,6,1248,130,58675,5,9,0,9119,6,3,5,10097,6,58675,5,8,0,10097,5];var MAIN_START_PC=0;var MAIN_REG_COUNT=12;var ENCODE_BYTECODE=false;var TIMING_CHECKS=false;var OP={["LOAD_CONST"]:58675,["LOAD_INT"]:59513,["LOAD_GLOBAL"]:9745,["LOAD_UPVALUE"]:62699,["LOAD_THIS"]:63815,["MOVE"]:51108,["STORE_GLOBAL"]:58865,["STORE_UPVALUE"]:5169,["GET_PROP"]:9119,["SET_PROP"]:21956,["DELETE_PROP"]:37517,["ADD"]:45047,["SUB"]:58367,["MUL"]:3343,["DIV"]:15331,["MOD"]:37910,["EXP"]:54744,["BAND"]:61072,["BOR"]:22373,["BXOR"]:49895,["SHL"]:24807,["SHR"]:39480,["USHR"]:1383,["LT"]:6858,["GT"]:50789,["LTE"]:49605,["GTE"]:62024,["EQ"]:37588,["NEQ"]:54099,["LOOSE_EQ"]:9938,["LOOSE_NEQ"]:31693,["IN"]:58249,["INSTANCEOF"]:55198,["UNARY_NEG"]:24245,["UNARY_POS"]:30173,["UNARY_NOT"]:24355,["UNARY_BITNOT"]:2075,["TYPEOF"]:42683,["VOID"]:2210,["TYPEOF_SAFE"]:31433,["JUMP"]:1248,["JUMP_IF_FALSE"]:62628,["JUMP_IF_TRUE"]:51402,["CALL"]:31339,["CALL_METHOD"]:49178,["NEW"]:45538,["RETURN"]:10097,["THROW"]:31958,["MAKE_CLOSURE"]:3126,["BUILD_ARRAY"]:32390,["BUILD_OBJECT"]:48733,["DEFINE_GETTER"]:31933,["DEFINE_SETTER"]:60588,["FOR_IN_SETUP"]:45797,["FOR_IN_NEXT"]:32563,["TRY_SETUP"]:2373,["TRY_END"]:56388,["PATCH"]:43666,["DEBUGGER"]:30171,["JUMP_REG"]:30352,["FINALLY_SETUP"]:4416};var SENTINELS={["CALL_SPREAD"]:1638052255};var SLOTS={["NOISE_END"]:6,["REG_BASE"]:9,["RET_DST"]:0,["THIS"]:3,["HANDLERS"]:11,["NOISE_PARAMS"]:10,["FRAME_SIZE"]:1,["CALLER"]:8,["PC"]:2,["CLOSURE"]:4};var HEADER_SIZE=13;var FRAME_START=16;function base64ToBytes(s){return typeof Buffer!=="undefined"?Buffer["from"](s,"base64"):Uint8Array["from"](atob(s),function(c){return c["charCodeAt"](0)})}function decodeBytecode(s){if(!ENCODE_BYTECODE){return s}var b=base64ToBytes(s);var r=new Uint32Array(b["length"]/4);for(var i=0;i>>0}return r}var CLOSURE_MAP=new WeakMap;function Upvalue(regs,absSlot){this["_regs"]=regs;this["_absSlot"]=absSlot;this["_closed"]=false;this["_value"]=undefined}Upvalue["prototype"]["_read"]=function(){return this["_closed"]?this["_value"]:this["_regs"][this["_absSlot"]]};Upvalue["prototype"]["_write"]=function(v){if(this["_closed"]){this["_value"]=v}else{this["_regs"][this["_absSlot"]]=v}};Upvalue["prototype"]["_close"]=function(){this["_value"]=this["_regs"][this["_absSlot"]];this["_closed"]=true};function Closure(fn){this["fn"]=fn;this["upvalues"]=[];this["prototype"]={}}function VM(bytecode,constants,globals){this["bytecode"]=bytecode;this["constants"]=constants;this["globals"]=globals;this["_openUpvalues"]=null;this["_regs"]=[];this["_regsTop"]=FRAME_START;this["_f"]=0}VM["prototype"]["_operand"]=function(){return this["bytecode"][this["_regs"][this["_f"]+SLOTS["PC"]]++]};VM["prototype"]["_pushFrame"]=function(closure,args,thisVal,retDst){var fn=closure["fn"];var regs=this["_regs"];var fp=this["_regsTop"];var size=HEADER_SIZE+fn["regCount"];var end=fp+size;while(regs["length"]>>13)&65535;var code=b[i*2]|b[i*2+1]<<8;out+=String["fromCharCode"](code^ks)}return out};VM["prototype"]["_closeUpvaluesFor"]=function(fp){var uvs=this["_openUpvalues"];if(!uvs){return}var regs=this["_regs"];var hi=fp+regs[fp+SLOTS["FRAME_SIZE"]];for(var i=regs[fp+SLOTS["REG_BASE"]];i{return performance["now"]()};var lastTime=now();this["_pushFrame"](closure,args,thisVal,0);while(true){var fp=this["_f"];var regs=this["_regs"];var bc=this["bytecode"];var pc=regs[fp+SLOTS["PC"]];if(pc>=bc["length"]){break}regs[fp+SLOTS["PC"]]=pc+1;var op=this["bytecode"][pc];if(typeof SLOTS["NOISE_PC"]==="number"){regs[fp+SLOTS["NOISE_PC"]]=(regs[fp+SLOTS["NOISE_PC"]]+1)%bc["length"]}if(typeof SLOTS["NOISE_COUNTER"]==="number"){regs[fp+SLOTS["NOISE_COUNTER"]]++}if(typeof SLOTS["NOISE_MIRROR"]==="number"){regs[fp+SLOTS["NOISE_MIRROR"]]=op}if(typeof SLOTS["NOISE_ACC"]==="number"){regs[fp+SLOTS["NOISE_ACC"]]=regs[fp+SLOTS["NOISE_ACC"]]^pc|0}if(TIMING_CHECKS){var currentTime=now();var isTamper=currentTime-lastTime>TIMING_CHECKS;lastTime=currentTime;if(isTamper){for(var i=0;i>regs[base+this["_operand"]()];break}case OP["USHR"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a>>>regs[base+this["_operand"]()];break}case OP["LT"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=aregs[base+this["_operand"]()];break}case OP["LTE"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a<=regs[base+this["_operand"]()];break}case OP["GTE"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a>=regs[base+this["_operand"]()];break}case OP["EQ"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a===regs[base+this["_operand"]()];break}case OP["NEQ"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a!==regs[base+this["_operand"]()];break}case OP["LOOSE_EQ"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a==regs[base+this["_operand"]()];break}case OP["LOOSE_NEQ"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a!=regs[base+this["_operand"]()];break}case OP["IN"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a in regs[base+this["_operand"]()];break}case OP["INSTANCEOF"]:{var dst=this["_operand"]();var obj=regs[base+this["_operand"]()];regs[base+dst]=obj instanceof regs[base+this["_operand"]()];break}case OP["UNARY_NEG"]:{var dst=this["_operand"]();regs[base+dst]=-regs[base+this["_operand"]()];break}case OP["UNARY_POS"]:{var dst=this["_operand"]();regs[base+dst]=+regs[base+this["_operand"]()];break}case OP["UNARY_NOT"]:{var dst=this["_operand"]();regs[base+dst]=!regs[base+this["_operand"]()];break}case OP["UNARY_BITNOT"]:{var dst=this["_operand"]();regs[base+dst]=~regs[base+this["_operand"]()];break}case OP["TYPEOF"]:{var dst=this["_operand"]();regs[base+dst]=typeof regs[base+this["_operand"]()];break}case OP["VOID"]:{var dst=this["_operand"]();this["_operand"]();regs[base+dst]=undefined;break}case OP["TYPEOF_SAFE"]:{var dst=this["_operand"]();var name=this["_constant"]();var val=Object["prototype"]["hasOwnProperty"]["call"](this["globals"],name)?this["globals"][name]:undefined;regs[base+dst]=typeof val;break}case OP["JUMP"]:regs[fp+SLOTS["PC"]]=this["_operand"]();break;case OP["JUMP_IF_FALSE"]:{var src=this["_operand"]();var target=this["_operand"]();if(!regs[base+src]){regs[fp+SLOTS["PC"]]=target}break}case OP["JUMP_IF_TRUE"]:{var src=this["_operand"]();var target=this["_operand"]();if(regs[base+src]){regs[fp+SLOTS["PC"]]=target}break}case OP["CALL"]:{var dst=this["_operand"]();var callee=regs[base+this["_operand"]()];var argc=this["_operand"]();var args;if(argc===SENTINELS["CALL_SPREAD"]){args=regs[base+this["_operand"]()]}else{args=new Array(argc);for(var i=0;i>1):0]=retVal;break}case OP["THROW"]:throw regs[base+this["_operand"]()];case OP["MAKE_CLOSURE"]:{var dst=this["_operand"]();var startPc=this["_operand"]();var paramCount=this["_operand"]();var regCount=this["_operand"]();var uvCount=this["_operand"]();var hasRest=this["_operand"]();var uvDescs=new Array(uvCount);for(var i=0;i=iter["_keys"]["length"]){regs[fp+SLOTS["PC"]]=exitTarget}else{regs[base+dst]=iter["_keys"][iter["i"]++]}break}case OP["TRY_SETUP"]:{var hs=regs[fp+SLOTS["HANDLERS"]];if(!hs){regs[fp+SLOTS["HANDLERS"]]=hs=[]}hs["push"]({["handlerPc"]:this["_operand"](),["exceptionReg"]:this["_operand"]()});break}case OP["TRY_END"]:{regs[fp+SLOTS["HANDLERS"]]["pop"]();break}case OP["FINALLY_SETUP"]:{var hs=regs[fp+SLOTS["HANDLERS"]];if(!hs){regs[fp+SLOTS["HANDLERS"]]=hs=[]}hs["push"]({["finallyPc"]:this["_operand"](),["contReg"]:this["_operand"](),["payloadReg"]:this["_operand"](),["throwPad"]:this["_operand"]()});break}case OP["PATCH"]:{var destPc=this["_operand"]();var sliceStart=this["_operand"]();var sliceEnd=this["_operand"]();var pk=this["_operand"]()^destPc|0;for(var pi=sliceStart;pi>>13))>>>0}break}case OP["JUMP_REG"]:{regs[fp+SLOTS["PC"]]=regs[base+this["_operand"]()];break}case OP["DEBUGGER"]:{debugger;break}default:throw new Error("Unknown opcode: "+op+" at pc "+(regs[fp+SLOTS["PC"]]-1))}}catch(err){var handledFrame=0;var searchFrame=this["_f"];while(searchFrame){var handlers=regs[searchFrame+SLOTS["HANDLERS"]];if(handlers&&handlers["length"]>0){handledFrame=searchFrame;break}this["_closeUpvaluesFor"](searchFrame);this["_regsTop"]=searchFrame;searchFrame=regs[searchFrame+SLOTS["CALLER"]];this["_f"]=searchFrame}if(!handledFrame){throw err}var h=regs[handledFrame+SLOTS["HANDLERS"]]["pop"]();var hBase=regs[handledFrame+SLOTS["REG_BASE"]];if(h["exceptionReg"]!==undefined){regs[hBase+h["exceptionReg"]]=err;regs[handledFrame+SLOTS["PC"]]=h["handlerPc"]}else{regs[hBase+h["contReg"]]=h["throwPad"];regs[hBase+h["payloadReg"]]=err;regs[handledFrame+SLOTS["PC"]]=h["finallyPc"]}this["_regsTop"]=handledFrame+regs[handledFrame+SLOTS["FRAME_SIZE"]];this["_f"]=handledFrame}if(!this["_f"]){return this["_regs"][0]}}};var globals=globalThis;if(typeof window!=="undefined"){globals["window"]=window;globals["document"]=typeof document!=="undefined"?document:undefined}if(typeof module!=="undefined"){globals["module"]=module;globals["exports"]=typeof exports!=="undefined"?exports:undefined}var vm=new VM(decodeBytecode(BYTECODE),CONSTANTS,globals);vm["run"](new Closure({["paramCount"]:0,["regCount"]:MAIN_REG_COUNT,["startPc"]:MAIN_START_PC}),undefined,null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/outer-layers/generation.json b/test/vm/jsconfuser-vm/fixtures/outer-layers/generation.json new file mode 100644 index 00000000..aa0f10d5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/outer-layers/generation.json @@ -0,0 +1,48 @@ +{ + "note": "vm.encoded.js is the VM encoder's output for source.js with vmOptions, the leading disassembly comment block removed (the committed corpus format). Each other file is the outer encoder's output over that VM output (comment block included) with target node and one listed option. Math.random was replaced by the seeded PRNG used by cff-loop-return (mulberry32: state += 0x6d2b79f5, then the standard mulberry32 output), reset to the seed before each encoder. Regenerating with the same encoder revisions, options and seed reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5" + }, + "outerEncoder": { + "revision": "31c5a47a79f97e4b4c2d4b2a8552c11a8b548fb0", + "version": "2.1.3" + }, + "seed": 324508639, + "vmOptions": { + "target": "browser", + "randomizeOpcodes": true + }, + "files": { + "compact.encoded.js": { + "outerOptions": { + "target": "node", + "compact": true + } + }, + "object-extraction.encoded.js": { + "outerOptions": { + "target": "node", + "objectExtraction": true + } + }, + "moved-declarations.encoded.js": { + "outerOptions": { + "target": "node", + "movedDeclarations": true + } + }, + "minify.encoded.js": { + "outerOptions": { + "target": "node", + "minify": true + } + }, + "variable-masking.encoded.js": { + "outerOptions": { + "target": "node", + "variableMasking": 1 + } + } + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/outer-layers/minify.encoded.js b/test/vm/jsconfuser-vm/fixtures/outer-layers/minify.encoded.js new file mode 100644 index 00000000..b2f66fa4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/outer-layers/minify.encoded.js @@ -0,0 +1 @@ +var CONSTANTS=["window","TEST_OUTPUT",4,"Math","max",2,7,100,void 0,"value",0,1],BYTECODE=[63815,1,3126,4,90,1,4,0,0,51108,2,4,3126,4,106,1,8,1,0,1,2,51108,3,4,9745,4,0,0,58675,5,1,0,58675,6,2,0,31339,7,3,1,6,9745,6,3,0,58675,8,4,0,9119,9,6,8,58675,8,5,0,58675,10,6,0,49178,11,6,9,2,8,10,58675,6,7,0,3343,8,11,6,45047,6,7,8,21956,4,5,6,58675,4,8,0,10097,4,63815,2,58675,3,9,0,21956,2,3,0,58675,3,8,0,10097,3,63815,2,62699,5,0,58675,6,10,0,45538,7,5,1,6,51108,3,7,58675,5,10,0,51108,4,5,6858,5,4,0,62628,5,169,58675,5,9,0,9119,6,3,5,45047,7,6,4,21956,3,5,7,51108,5,4,58675,5,11,0,45047,6,4,5,51108,4,6,1248,130,58675,5,9,0,9119,6,3,5,10097,6,58675,5,8,0,10097,5],MAIN_START_PC=0,MAIN_REG_COUNT=12,ENCODE_BYTECODE=!1,TIMING_CHECKS=!1,OP={LOAD_CONST:58675,LOAD_INT:59513,LOAD_GLOBAL:9745,LOAD_UPVALUE:62699,LOAD_THIS:63815,MOVE:51108,STORE_GLOBAL:58865,STORE_UPVALUE:5169,GET_PROP:9119,SET_PROP:21956,DELETE_PROP:37517,ADD:45047,SUB:58367,MUL:3343,DIV:15331,MOD:37910,EXP:54744,BAND:61072,BOR:22373,BXOR:49895,SHL:24807,SHR:39480,USHR:1383,LT:6858,GT:50789,LTE:49605,GTE:62024,EQ:37588,NEQ:54099,LOOSE_EQ:9938,LOOSE_NEQ:31693,IN:58249,INSTANCEOF:55198,UNARY_NEG:24245,UNARY_POS:30173,UNARY_NOT:24355,UNARY_BITNOT:2075,TYPEOF:42683,VOID:2210,TYPEOF_SAFE:31433,JUMP:1248,JUMP_IF_FALSE:62628,JUMP_IF_TRUE:51402,CALL:31339,CALL_METHOD:49178,NEW:45538,RETURN:10097,THROW:31958,MAKE_CLOSURE:3126,BUILD_ARRAY:32390,BUILD_OBJECT:48733,DEFINE_GETTER:31933,DEFINE_SETTER:60588,FOR_IN_SETUP:45797,FOR_IN_NEXT:32563,TRY_SETUP:2373,TRY_END:56388,PATCH:43666,DEBUGGER:30171,JUMP_REG:30352,FINALLY_SETUP:4416},SENTINELS={CALL_SPREAD:1638052255},SLOTS={NOISE_END:6,REG_BASE:9,RET_DST:0,THIS:3,HANDLERS:11,NOISE_PARAMS:10,FRAME_SIZE:1,CALLER:8,PC:2,CLOSURE:4},HEADER_SIZE=13,FRAME_START=16;function base64ToBytes(s){return typeof Buffer!=="undefined"?Buffer.from(s,"base64"):Uint8Array.from(atob(s),function(c){return c.charCodeAt(0)})}function decodeBytecode(s){if(!ENCODE_BYTECODE){return s}var b=base64ToBytes(s),r=new Uint32Array(b.length/4);for(var i=0;i>>0;return r}var CLOSURE_MAP=new WeakMap;function Upvalue(regs,absSlot){this._regs=regs;this._absSlot=absSlot;this._closed=!1;this._value=void 0}Upvalue.prototype._read=function(){return this._closed?this._value:this._regs[this._absSlot]};Upvalue.prototype._write=function(v){this._closed?this._value=v:this._regs[this._absSlot]=v};Upvalue.prototype._close=function(){this._value=this._regs[this._absSlot];this._closed=!0};function Closure(fn){this.fn=fn;this.upvalues=[];this.prototype={}}function VM(bytecode,constants,globals){this.bytecode=bytecode;this.constants=constants;this.globals=globals;this._openUpvalues=null;this._regs=[];this._regsTop=FRAME_START;this._f=0}VM.prototype._operand=function(){return this.bytecode[this._regs[this._f+SLOTS.PC]++]};VM.prototype._pushFrame=function(closure,args,thisVal,retDst){var fn=closure.fn,regs=this._regs,fp=this._regsTop,size=HEADER_SIZE+fn.regCount,end=fp+size;while(regs.length>>13)&65535,code=b[i*2]|b[i*2+1]<<8;out+=String.fromCharCode(code^ks)}return out};VM.prototype._closeUpvaluesFor=function(fp){var uvs=this._openUpvalues;if(!uvs){return}var regs=this._regs,hi=fp+regs[fp+SLOTS.FRAME_SIZE];for(var i=regs[fp+SLOTS.REG_BASE];i{return performance.now()},lastTime=now();this._pushFrame(closure,args,thisVal,0);while(!0){var fp=this._f,regs=this._regs,bc=this.bytecode,pc=regs[fp+SLOTS.PC];if(pc>=bc.length){break}regs[fp+SLOTS.PC]=pc+1;var op=this.bytecode[pc];if(typeof SLOTS.NOISE_PC==="number"){regs[fp+SLOTS.NOISE_PC]=(regs[fp+SLOTS.NOISE_PC]+1)%bc.length}if(typeof SLOTS.NOISE_COUNTER==="number"){regs[fp+SLOTS.NOISE_COUNTER]++}if(typeof SLOTS.NOISE_MIRROR==="number"){regs[fp+SLOTS.NOISE_MIRROR]=op}if(typeof SLOTS.NOISE_ACC==="number"){regs[fp+SLOTS.NOISE_ACC]=regs[fp+SLOTS.NOISE_ACC]^pc|0}if(TIMING_CHECKS){var currentTime=now(),isTamper=currentTime-lastTime>TIMING_CHECKS;lastTime=currentTime;if(isTamper){for(var i=0;i>regs[base+this._operand()];break}case OP.USHR:{var dst=this._operand(),a=regs[base+this._operand()];regs[base+dst]=a>>>regs[base+this._operand()];break}case OP.LT:{var dst=this._operand(),a=regs[base+this._operand()];regs[base+dst]=aregs[base+this._operand()];break}case OP.LTE:{var dst=this._operand(),a=regs[base+this._operand()];regs[base+dst]=a<=regs[base+this._operand()];break}case OP.GTE:{var dst=this._operand(),a=regs[base+this._operand()];regs[base+dst]=a>=regs[base+this._operand()];break}case OP.EQ:{var dst=this._operand(),a=regs[base+this._operand()];regs[base+dst]=a===regs[base+this._operand()];break}case OP.NEQ:{var dst=this._operand(),a=regs[base+this._operand()];regs[base+dst]=a!==regs[base+this._operand()];break}case OP.LOOSE_EQ:{var dst=this._operand(),a=regs[base+this._operand()];regs[base+dst]=a==regs[base+this._operand()];break}case OP.LOOSE_NEQ:{var dst=this._operand(),a=regs[base+this._operand()];regs[base+dst]=a!=regs[base+this._operand()];break}case OP.IN:{var dst=this._operand(),a=regs[base+this._operand()];regs[base+dst]=a in regs[base+this._operand()];break}case OP.INSTANCEOF:{var dst=this._operand(),obj=regs[base+this._operand()];regs[base+dst]=obj instanceof regs[base+this._operand()];break}case OP.UNARY_NEG:{var dst=this._operand();regs[base+dst]=-regs[base+this._operand()];break}case OP.UNARY_POS:{var dst=this._operand();regs[base+dst]=+regs[base+this._operand()];break}case OP.UNARY_NOT:{var dst=this._operand();regs[base+dst]=!regs[base+this._operand()];break}case OP.UNARY_BITNOT:{var dst=this._operand();regs[base+dst]=~regs[base+this._operand()];break}case OP.TYPEOF:{var dst=this._operand();regs[base+dst]=typeof regs[base+this._operand()];break}case OP.VOID:{var dst=this._operand();this._operand();regs[base+dst]=void 0;break}case OP.TYPEOF_SAFE:{var dst=this._operand(),name=this._constant(),val=Object.prototype.hasOwnProperty.call(this.globals,name)?this.globals[name]:void 0;regs[base+dst]=typeof val;break}case OP.JUMP:regs[fp+SLOTS.PC]=this._operand();break;case OP.JUMP_IF_FALSE:{var src=this._operand(),target=this._operand();if(!regs[base+src]){regs[fp+SLOTS.PC]=target}break}case OP.JUMP_IF_TRUE:{var src=this._operand(),target=this._operand();if(regs[base+src]){regs[fp+SLOTS.PC]=target}break}case OP.CALL:{var dst=this._operand(),callee=regs[base+this._operand()],argc=this._operand(),args;if(argc===SENTINELS.CALL_SPREAD){args=regs[base+this._operand()]}else{args=new Array(argc);for(var i=0;i>1):0]=retVal;break}case OP.THROW:throw regs[base+this._operand()];case OP.MAKE_CLOSURE:{var dst=this._operand(),startPc=this._operand(),paramCount=this._operand(),regCount=this._operand(),uvCount=this._operand(),hasRest=this._operand(),uvDescs=new Array(uvCount);for(var i=0;i=iter._keys.length?regs[fp+SLOTS.PC]=exitTarget:regs[base+dst]=iter._keys[iter.i++];break}case OP.TRY_SETUP:{var hs=regs[fp+SLOTS.HANDLERS];if(!hs){regs[fp+SLOTS.HANDLERS]=hs=[]}hs.push({handlerPc:this._operand(),exceptionReg:this._operand()});break}case OP.TRY_END:{regs[fp+SLOTS.HANDLERS].pop();break}case OP.FINALLY_SETUP:{var hs=regs[fp+SLOTS.HANDLERS];if(!hs){regs[fp+SLOTS.HANDLERS]=hs=[]}hs.push({finallyPc:this._operand(),contReg:this._operand(),payloadReg:this._operand(),throwPad:this._operand()});break}case OP.PATCH:{var destPc=this._operand(),sliceStart=this._operand(),sliceEnd=this._operand(),pk=this._operand()^destPc|0;for(var pi=sliceStart;pi>>13))>>>0}break}case OP.JUMP_REG:{regs[fp+SLOTS.PC]=regs[base+this._operand()];break}case OP.DEBUGGER:{debugger;break}default:throw new Error("Unknown opcode: "+op+" at pc "+(regs[fp+SLOTS.PC]-1))}}catch(err){var handledFrame=0,searchFrame=this._f;while(searchFrame){var handlers=regs[searchFrame+SLOTS.HANDLERS];if(handlers&&handlers.length>0){handledFrame=searchFrame;break}this._closeUpvaluesFor(searchFrame);this._regsTop=searchFrame;searchFrame=regs[searchFrame+SLOTS.CALLER];this._f=searchFrame}if(!handledFrame){throw err}var h=regs[handledFrame+SLOTS.HANDLERS].pop(),hBase=regs[handledFrame+SLOTS.REG_BASE];h.exceptionReg!==void 0?(regs[hBase+h.exceptionReg]=err,regs[handledFrame+SLOTS.PC]=h.handlerPc):(regs[hBase+h.contReg]=h.throwPad,regs[hBase+h.payloadReg]=err,regs[handledFrame+SLOTS.PC]=h.finallyPc);this._regsTop=handledFrame+regs[handledFrame+SLOTS.FRAME_SIZE];this._f=handledFrame}if(!this._f){return this._regs[0]}}};var globals=globalThis;if(typeof window!=="undefined"){globals.window=window;globals.document=typeof document!=="undefined"?document:void 0}if(typeof module!=="undefined"){globals.module=module;globals.exports=typeof exports!=="undefined"?exports:void 0}var vm=new VM(decodeBytecode(BYTECODE),CONSTANTS,globals);vm.run(new Closure({paramCount:0,regCount:MAIN_REG_COUNT,startPc:MAIN_START_PC}),void 0,null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/outer-layers/moved-declarations.encoded.js b/test/vm/jsconfuser-vm/fixtures/outer-layers/moved-declarations.encoded.js new file mode 100644 index 00000000..f270a67e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/outer-layers/moved-declarations.encoded.js @@ -0,0 +1 @@ +var CONSTANTS=["window","TEST_OUTPUT",4,"Math","max",2,7,100,undefined,"value",0,1],BYTECODE,MAIN_START_PC,MAIN_REG_COUNT,ENCODE_BYTECODE,TIMING_CHECKS,OP,SENTINELS,SLOTS,HEADER_SIZE,FRAME_START,CLOSURE_MAP,globals,vm;BYTECODE=[63815,1,3126,4,90,1,4,0,0,51108,2,4,3126,4,106,1,8,1,0,1,2,51108,3,4,9745,4,0,0,58675,5,1,0,58675,6,2,0,31339,7,3,1,6,9745,6,3,0,58675,8,4,0,9119,9,6,8,58675,8,5,0,58675,10,6,0,49178,11,6,9,2,8,10,58675,6,7,0,3343,8,11,6,45047,6,7,8,21956,4,5,6,58675,4,8,0,10097,4,63815,2,58675,3,9,0,21956,2,3,0,58675,3,8,0,10097,3,63815,2,62699,5,0,58675,6,10,0,45538,7,5,1,6,51108,3,7,58675,5,10,0,51108,4,5,6858,5,4,0,62628,5,169,58675,5,9,0,9119,6,3,5,45047,7,6,4,21956,3,5,7,51108,5,4,58675,5,11,0,45047,6,4,5,51108,4,6,1248,130,58675,5,9,0,9119,6,3,5,10097,6,58675,5,8,0,10097,5];MAIN_START_PC=0;MAIN_REG_COUNT=12;ENCODE_BYTECODE=false;TIMING_CHECKS=false;OP={["LOAD_CONST"]:58675,["LOAD_INT"]:59513,["LOAD_GLOBAL"]:9745,["LOAD_UPVALUE"]:62699,["LOAD_THIS"]:63815,["MOVE"]:51108,["STORE_GLOBAL"]:58865,["STORE_UPVALUE"]:5169,["GET_PROP"]:9119,["SET_PROP"]:21956,["DELETE_PROP"]:37517,["ADD"]:45047,["SUB"]:58367,["MUL"]:3343,["DIV"]:15331,["MOD"]:37910,["EXP"]:54744,["BAND"]:61072,["BOR"]:22373,["BXOR"]:49895,["SHL"]:24807,["SHR"]:39480,["USHR"]:1383,["LT"]:6858,["GT"]:50789,["LTE"]:49605,["GTE"]:62024,["EQ"]:37588,["NEQ"]:54099,["LOOSE_EQ"]:9938,["LOOSE_NEQ"]:31693,["IN"]:58249,["INSTANCEOF"]:55198,["UNARY_NEG"]:24245,["UNARY_POS"]:30173,["UNARY_NOT"]:24355,["UNARY_BITNOT"]:2075,["TYPEOF"]:42683,["VOID"]:2210,["TYPEOF_SAFE"]:31433,["JUMP"]:1248,["JUMP_IF_FALSE"]:62628,["JUMP_IF_TRUE"]:51402,["CALL"]:31339,["CALL_METHOD"]:49178,["NEW"]:45538,["RETURN"]:10097,["THROW"]:31958,["MAKE_CLOSURE"]:3126,["BUILD_ARRAY"]:32390,["BUILD_OBJECT"]:48733,["DEFINE_GETTER"]:31933,["DEFINE_SETTER"]:60588,["FOR_IN_SETUP"]:45797,["FOR_IN_NEXT"]:32563,["TRY_SETUP"]:2373,["TRY_END"]:56388,["PATCH"]:43666,["DEBUGGER"]:30171,["JUMP_REG"]:30352,["FINALLY_SETUP"]:4416};SENTINELS={["CALL_SPREAD"]:1638052255};SLOTS={["NOISE_END"]:6,["REG_BASE"]:9,["RET_DST"]:0,["THIS"]:3,["HANDLERS"]:11,["NOISE_PARAMS"]:10,["FRAME_SIZE"]:1,["CALLER"]:8,["PC"]:2,["CLOSURE"]:4};HEADER_SIZE=13;FRAME_START=16;function base64ToBytes(s){return typeof Buffer!=="undefined"?Buffer["from"](s,"base64"):Uint8Array["from"](atob(s),function(c){return c["charCodeAt"](0)})}function decodeBytecode(s,b,r,i){if(!ENCODE_BYTECODE){return s}b=base64ToBytes(s);r=new Uint32Array(b["length"]/4);for(i=0;i>>0}return r}CLOSURE_MAP=new WeakMap;function Upvalue(regs,absSlot){this["_regs"]=regs;this["_absSlot"]=absSlot;this["_closed"]=false;this["_value"]=undefined}Upvalue["prototype"]["_read"]=function(){return this["_closed"]?this["_value"]:this["_regs"][this["_absSlot"]]};Upvalue["prototype"]["_write"]=function(v){if(this["_closed"]){this["_value"]=v}else{this["_regs"][this["_absSlot"]]=v}};Upvalue["prototype"]["_close"]=function(){this["_value"]=this["_regs"][this["_absSlot"]];this["_closed"]=true};function Closure(fn){this["fn"]=fn;this["upvalues"]=[];this["prototype"]={}}function VM(bytecode,constants,globals){this["bytecode"]=bytecode;this["constants"]=constants;this["globals"]=globals;this["_openUpvalues"]=null;this["_regs"]=[];this["_regsTop"]=FRAME_START;this["_f"]=0}VM["prototype"]["_operand"]=function(){return this["bytecode"][this["_regs"][this["_f"]+SLOTS["PC"]]++]};VM["prototype"]["_pushFrame"]=function(closure,args,thisVal,retDst){var fn=closure["fn"],regs,fp,size,end,i,base;regs=this["_regs"];fp=this["_regsTop"];size=HEADER_SIZE+fn["regCount"];end=fp+size;while(regs["length"]>>13)&65535;code=b[i*2]|b[i*2+1]<<8;out+=String["fromCharCode"](code^ks)}return out};VM["prototype"]["_closeUpvaluesFor"]=function(fp){var uvs=this["_openUpvalues"],regs,hi,i;if(!uvs){return}regs=this["_regs"];hi=fp+regs[fp+SLOTS["FRAME_SIZE"]];for(i=regs[fp+SLOTS["REG_BASE"]];i{return performance["now"]()},lastTime;lastTime=now();this["_pushFrame"](closure,args,thisVal,0);while(true){var fp=this["_f"],regs,bc,pc,op;regs=this["_regs"];bc=this["bytecode"];pc=regs[fp+SLOTS["PC"]];if(pc>=bc["length"]){break}regs[fp+SLOTS["PC"]]=pc+1;op=this["bytecode"][pc];if(typeof SLOTS["NOISE_PC"]==="number"){regs[fp+SLOTS["NOISE_PC"]]=(regs[fp+SLOTS["NOISE_PC"]]+1)%bc["length"]}if(typeof SLOTS["NOISE_COUNTER"]==="number"){regs[fp+SLOTS["NOISE_COUNTER"]]++}if(typeof SLOTS["NOISE_MIRROR"]==="number"){regs[fp+SLOTS["NOISE_MIRROR"]]=op}if(typeof SLOTS["NOISE_ACC"]==="number"){regs[fp+SLOTS["NOISE_ACC"]]=regs[fp+SLOTS["NOISE_ACC"]]^pc|0}if(TIMING_CHECKS){var currentTime=now(),isTamper;isTamper=currentTime-lastTime>TIMING_CHECKS;lastTime=currentTime;if(isTamper){var i,i2;for(i=0;i>regs[base+this["_operand"]()];break}case OP["USHR"]:{var dst=this["_operand"](),a;a=regs[base+this["_operand"]()];regs[base+dst]=a>>>regs[base+this["_operand"]()];break}case OP["LT"]:{var dst=this["_operand"](),a;a=regs[base+this["_operand"]()];regs[base+dst]=aregs[base+this["_operand"]()];break}case OP["LTE"]:{var dst=this["_operand"](),a;a=regs[base+this["_operand"]()];regs[base+dst]=a<=regs[base+this["_operand"]()];break}case OP["GTE"]:{var dst=this["_operand"](),a;a=regs[base+this["_operand"]()];regs[base+dst]=a>=regs[base+this["_operand"]()];break}case OP["EQ"]:{var dst=this["_operand"](),a;a=regs[base+this["_operand"]()];regs[base+dst]=a===regs[base+this["_operand"]()];break}case OP["NEQ"]:{var dst=this["_operand"](),a;a=regs[base+this["_operand"]()];regs[base+dst]=a!==regs[base+this["_operand"]()];break}case OP["LOOSE_EQ"]:{var dst=this["_operand"](),a;a=regs[base+this["_operand"]()];regs[base+dst]=a==regs[base+this["_operand"]()];break}case OP["LOOSE_NEQ"]:{var dst=this["_operand"](),a;a=regs[base+this["_operand"]()];regs[base+dst]=a!=regs[base+this["_operand"]()];break}case OP["IN"]:{var dst=this["_operand"](),a;a=regs[base+this["_operand"]()];regs[base+dst]=a in regs[base+this["_operand"]()];break}case OP["INSTANCEOF"]:{var dst=this["_operand"](),obj;obj=regs[base+this["_operand"]()];regs[base+dst]=obj instanceof regs[base+this["_operand"]()];break}case OP["UNARY_NEG"]:{var dst=this["_operand"]();regs[base+dst]=-regs[base+this["_operand"]()];break}case OP["UNARY_POS"]:{var dst=this["_operand"]();regs[base+dst]=+regs[base+this["_operand"]()];break}case OP["UNARY_NOT"]:{var dst=this["_operand"]();regs[base+dst]=!regs[base+this["_operand"]()];break}case OP["UNARY_BITNOT"]:{var dst=this["_operand"]();regs[base+dst]=~regs[base+this["_operand"]()];break}case OP["TYPEOF"]:{var dst=this["_operand"]();regs[base+dst]=typeof regs[base+this["_operand"]()];break}case OP["VOID"]:{var dst=this["_operand"]();this["_operand"]();regs[base+dst]=undefined;break}case OP["TYPEOF_SAFE"]:{var dst=this["_operand"](),name,val;name=this["_constant"]();val=Object["prototype"]["hasOwnProperty"]["call"](this["globals"],name)?this["globals"][name]:undefined;regs[base+dst]=typeof val;break}case OP["JUMP"]:regs[fp+SLOTS["PC"]]=this["_operand"]();break;case OP["JUMP_IF_FALSE"]:{var src=this["_operand"](),target;target=this["_operand"]();if(!regs[base+src]){regs[fp+SLOTS["PC"]]=target}break}case OP["JUMP_IF_TRUE"]:{var src=this["_operand"](),target;target=this["_operand"]();if(regs[base+src]){regs[fp+SLOTS["PC"]]=target}break}case OP["CALL"]:{var dst=this["_operand"](),callee,argc,args,closure;callee=regs[base+this["_operand"]()];argc=this["_operand"]();args=undefined;if(argc===SENTINELS["CALL_SPREAD"]){args=regs[base+this["_operand"]()]}else{var i;args=new Array(argc);for(i=0;i>1):0]=retVal;break}case OP["THROW"]:throw regs[base+this["_operand"]()];case OP["MAKE_CLOSURE"]:{var dst=this["_operand"](),startPc,paramCount,regCount,uvCount,hasRest,uvDescs,i,fn,closure,i,self,shell;startPc=this["_operand"]();paramCount=this["_operand"]();regCount=this["_operand"]();uvCount=this["_operand"]();hasRest=this["_operand"]();uvDescs=new Array(uvCount);for(i=0;i=iter["_keys"]["length"]){regs[fp+SLOTS["PC"]]=exitTarget}else{regs[base+dst]=iter["_keys"][iter["i"]++]}break}case OP["TRY_SETUP"]:{var hs=regs[fp+SLOTS["HANDLERS"]];if(!hs){regs[fp+SLOTS["HANDLERS"]]=hs=[]}hs["push"]({["handlerPc"]:this["_operand"](),["exceptionReg"]:this["_operand"]()});break}case OP["TRY_END"]:{regs[fp+SLOTS["HANDLERS"]]["pop"]();break}case OP["FINALLY_SETUP"]:{var hs=regs[fp+SLOTS["HANDLERS"]];if(!hs){regs[fp+SLOTS["HANDLERS"]]=hs=[]}hs["push"]({["finallyPc"]:this["_operand"](),["contReg"]:this["_operand"](),["payloadReg"]:this["_operand"](),["throwPad"]:this["_operand"]()});break}case OP["PATCH"]:{var destPc=this["_operand"](),sliceStart,sliceEnd,pk,pi;sliceStart=this["_operand"]();sliceEnd=this["_operand"]();pk=this["_operand"]()^destPc|0;for(pi=sliceStart;pi>>13))>>>0}break}case OP["JUMP_REG"]:{regs[fp+SLOTS["PC"]]=regs[base+this["_operand"]()];break}case OP["DEBUGGER"]:{debugger;break}default:throw new Error("Unknown opcode: "+op+" at pc "+(regs[fp+SLOTS["PC"]]-1))}}catch(err){var handledFrame=0,searchFrame,h,hBase;searchFrame=this["_f"];while(searchFrame){var handlers=regs[searchFrame+SLOTS["HANDLERS"]];if(handlers&&handlers["length"]>0){handledFrame=searchFrame;break}this["_closeUpvaluesFor"](searchFrame);this["_regsTop"]=searchFrame;searchFrame=regs[searchFrame+SLOTS["CALLER"]];this["_f"]=searchFrame}if(!handledFrame){throw err}h=regs[handledFrame+SLOTS["HANDLERS"]]["pop"]();hBase=regs[handledFrame+SLOTS["REG_BASE"]];if(h["exceptionReg"]!==undefined){regs[hBase+h["exceptionReg"]]=err;regs[handledFrame+SLOTS["PC"]]=h["handlerPc"]}else{regs[hBase+h["contReg"]]=h["throwPad"];regs[hBase+h["payloadReg"]]=err;regs[handledFrame+SLOTS["PC"]]=h["finallyPc"]}this["_regsTop"]=handledFrame+regs[handledFrame+SLOTS["FRAME_SIZE"]];this["_f"]=handledFrame}if(!this["_f"]){return this["_regs"][0]}}};globals=globalThis;if(typeof window!=="undefined"){globals["window"]=window;globals["document"]=typeof document!=="undefined"?document:undefined}if(typeof module!=="undefined"){globals["module"]=module;globals["exports"]=typeof exports!=="undefined"?exports:undefined}vm=new VM(decodeBytecode(BYTECODE),CONSTANTS,globals);vm["run"](new Closure({["paramCount"]:0,["regCount"]:MAIN_REG_COUNT,["startPc"]:MAIN_START_PC}),undefined,null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/outer-layers/object-extraction.encoded.js b/test/vm/jsconfuser-vm/fixtures/outer-layers/object-extraction.encoded.js new file mode 100644 index 00000000..ead88a4e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/outer-layers/object-extraction.encoded.js @@ -0,0 +1 @@ +var CONSTANTS=["window","TEST_OUTPUT",4,"Math","max",2,7,100,undefined,"value",0,1];var BYTECODE=[63815,1,3126,4,90,1,4,0,0,51108,2,4,3126,4,106,1,8,1,0,1,2,51108,3,4,9745,4,0,0,58675,5,1,0,58675,6,2,0,31339,7,3,1,6,9745,6,3,0,58675,8,4,0,9119,9,6,8,58675,8,5,0,58675,10,6,0,49178,11,6,9,2,8,10,58675,6,7,0,3343,8,11,6,45047,6,7,8,21956,4,5,6,58675,4,8,0,10097,4,63815,2,58675,3,9,0,21956,2,3,0,58675,3,8,0,10097,3,63815,2,62699,5,0,58675,6,10,0,45538,7,5,1,6,51108,3,7,58675,5,10,0,51108,4,5,6858,5,4,0,62628,5,169,58675,5,9,0,9119,6,3,5,45047,7,6,4,21956,3,5,7,51108,5,4,58675,5,11,0,45047,6,4,5,51108,4,6,1248,130,58675,5,9,0,9119,6,3,5,10097,6,58675,5,8,0,10097,5];var MAIN_START_PC=0;var MAIN_REG_COUNT=12;var ENCODE_BYTECODE=false;var TIMING_CHECKS=false;var __p_BmUS_OP_LOAD_CONST=58675;var __p_BmUS_OP_LOAD_INT=59513;var __p_BmUS_OP_LOAD_GLOBAL=9745;var __p_BmUS_OP_LOAD_UPVALUE=62699;var __p_BmUS_OP_LOAD_THIS=63815;var __p_BmUS_OP_MOVE=51108;var __p_BmUS_OP_STORE_GLOBAL=58865;var __p_BmUS_OP_STORE_UPVALUE=5169;var __p_BmUS_OP_GET_PROP=9119;var __p_BmUS_OP_SET_PROP=21956;var __p_BmUS_OP_DELETE_PROP=37517;var __p_BmUS_OP_ADD=45047;var __p_BmUS_OP_SUB=58367;var __p_BmUS_OP_MUL=3343;var __p_BmUS_OP_DIV=15331;var __p_BmUS_OP_MOD=37910;var __p_BmUS_OP_EXP=54744;var __p_BmUS_OP_BAND=61072;var __p_BmUS_OP_BOR=22373;var __p_BmUS_OP_BXOR=49895;var __p_BmUS_OP_SHL=24807;var __p_BmUS_OP_SHR=39480;var __p_BmUS_OP_USHR=1383;var __p_BmUS_OP_LT=6858;var __p_BmUS_OP_GT=50789;var __p_BmUS_OP_LTE=49605;var __p_BmUS_OP_GTE=62024;var __p_BmUS_OP_EQ=37588;var __p_BmUS_OP_NEQ=54099;var __p_BmUS_OP_LOOSE_EQ=9938;var __p_BmUS_OP_LOOSE_NEQ=31693;var __p_BmUS_OP_IN=58249;var __p_BmUS_OP_INSTANCEOF=55198;var __p_BmUS_OP_UNARY_NEG=24245;var __p_BmUS_OP_UNARY_POS=30173;var __p_BmUS_OP_UNARY_NOT=24355;var __p_BmUS_OP_UNARY_BITNOT=2075;var __p_BmUS_OP_TYPEOF=42683;var __p_BmUS_OP_VOID=2210;var __p_BmUS_OP_TYPEOF_SAFE=31433;var __p_BmUS_OP_JUMP=1248;var __p_BmUS_OP_JUMP_IF_FALSE=62628;var __p_BmUS_OP_JUMP_IF_TRUE=51402;var __p_BmUS_OP_CALL=31339;var __p_BmUS_OP_CALL_METHOD=49178;var __p_BmUS_OP_NEW=45538;var __p_BmUS_OP_RETURN=10097;var __p_BmUS_OP_THROW=31958;var __p_BmUS_OP_MAKE_CLOSURE=3126;var __p_BmUS_OP_BUILD_ARRAY=32390;var __p_BmUS_OP_BUILD_OBJECT=48733;var __p_BmUS_OP_DEFINE_GETTER=31933;var __p_BmUS_OP_DEFINE_SETTER=60588;var __p_BmUS_OP_FOR_IN_SETUP=45797;var __p_BmUS_OP_FOR_IN_NEXT=32563;var __p_BmUS_OP_TRY_SETUP=2373;var __p_BmUS_OP_TRY_END=56388;var __p_BmUS_OP_PATCH=43666;var __p_BmUS_OP_DEBUGGER=30171;var __p_BmUS_OP_JUMP_REG=30352;var __p_BmUS_OP_FINALLY_SETUP=4416;var __p_2AtS_SENTINELS_CALL_SPREAD=1638052255;var SLOTS={["NOISE_END"]:6,["REG_BASE"]:9,["RET_DST"]:0,["THIS"]:3,["HANDLERS"]:11,["NOISE_PARAMS"]:10,["FRAME_SIZE"]:1,["CALLER"]:8,["PC"]:2,["CLOSURE"]:4};var HEADER_SIZE=13;var FRAME_START=16;function base64ToBytes(s){return typeof Buffer!=="undefined"?Buffer["from"](s,"base64"):Uint8Array["from"](atob(s),function(c){return c["charCodeAt"](0)})}function decodeBytecode(s){if(!ENCODE_BYTECODE){return s}var b=base64ToBytes(s);var r=new Uint32Array(b["length"]/4);for(var i=0;i>>0}return r}var CLOSURE_MAP=new WeakMap;function Upvalue(regs,absSlot){this["_regs"]=regs;this["_absSlot"]=absSlot;this["_closed"]=false;this["_value"]=undefined}Upvalue["prototype"]["_read"]=function(){return this["_closed"]?this["_value"]:this["_regs"][this["_absSlot"]]};Upvalue["prototype"]["_write"]=function(v){if(this["_closed"]){this["_value"]=v}else{this["_regs"][this["_absSlot"]]=v}};Upvalue["prototype"]["_close"]=function(){this["_value"]=this["_regs"][this["_absSlot"]];this["_closed"]=true};function Closure(fn){this["fn"]=fn;this["upvalues"]=[];this["prototype"]={}}function VM(bytecode,constants,globals){this["bytecode"]=bytecode;this["constants"]=constants;this["globals"]=globals;this["_openUpvalues"]=null;this["_regs"]=[];this["_regsTop"]=FRAME_START;this["_f"]=0}VM["prototype"]["_operand"]=function(){return this["bytecode"][this["_regs"][this["_f"]+SLOTS["PC"]]++]};VM["prototype"]["_pushFrame"]=function(closure,args,thisVal,retDst){var fn=closure["fn"];var regs=this["_regs"];var fp=this["_regsTop"];var size=HEADER_SIZE+fn["regCount"];var end=fp+size;while(regs["length"]>>13)&65535;var code=b[i*2]|b[i*2+1]<<8;out+=String["fromCharCode"](code^ks)}return out};VM["prototype"]["_closeUpvaluesFor"]=function(fp){var uvs=this["_openUpvalues"];if(!uvs){return}var regs=this["_regs"];var hi=fp+regs[fp+SLOTS["FRAME_SIZE"]];for(var i=regs[fp+SLOTS["REG_BASE"]];i{return performance["now"]()};var lastTime=now();this["_pushFrame"](closure,args,thisVal,0);while(true){var fp=this["_f"];var regs=this["_regs"];var bc=this["bytecode"];var pc=regs[fp+SLOTS["PC"]];if(pc>=bc["length"]){break}regs[fp+SLOTS["PC"]]=pc+1;var op=this["bytecode"][pc];if(typeof SLOTS["NOISE_PC"]==="number"){regs[fp+SLOTS["NOISE_PC"]]=(regs[fp+SLOTS["NOISE_PC"]]+1)%bc["length"]}if(typeof SLOTS["NOISE_COUNTER"]==="number"){regs[fp+SLOTS["NOISE_COUNTER"]]++}if(typeof SLOTS["NOISE_MIRROR"]==="number"){regs[fp+SLOTS["NOISE_MIRROR"]]=op}if(typeof SLOTS["NOISE_ACC"]==="number"){regs[fp+SLOTS["NOISE_ACC"]]=regs[fp+SLOTS["NOISE_ACC"]]^pc|0}if(TIMING_CHECKS){var currentTime=now();var isTamper=currentTime-lastTime>TIMING_CHECKS;lastTime=currentTime;if(isTamper){for(var i=0;i>regs[base+this["_operand"]()];break}case __p_BmUS_OP_USHR:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a>>>regs[base+this["_operand"]()];break}case __p_BmUS_OP_LT:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=aregs[base+this["_operand"]()];break}case __p_BmUS_OP_LTE:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a<=regs[base+this["_operand"]()];break}case __p_BmUS_OP_GTE:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a>=regs[base+this["_operand"]()];break}case __p_BmUS_OP_EQ:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a===regs[base+this["_operand"]()];break}case __p_BmUS_OP_NEQ:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a!==regs[base+this["_operand"]()];break}case __p_BmUS_OP_LOOSE_EQ:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a==regs[base+this["_operand"]()];break}case __p_BmUS_OP_LOOSE_NEQ:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a!=regs[base+this["_operand"]()];break}case __p_BmUS_OP_IN:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a in regs[base+this["_operand"]()];break}case __p_BmUS_OP_INSTANCEOF:{var dst=this["_operand"]();var obj=regs[base+this["_operand"]()];regs[base+dst]=obj instanceof regs[base+this["_operand"]()];break}case __p_BmUS_OP_UNARY_NEG:{var dst=this["_operand"]();regs[base+dst]=-regs[base+this["_operand"]()];break}case __p_BmUS_OP_UNARY_POS:{var dst=this["_operand"]();regs[base+dst]=+regs[base+this["_operand"]()];break}case __p_BmUS_OP_UNARY_NOT:{var dst=this["_operand"]();regs[base+dst]=!regs[base+this["_operand"]()];break}case __p_BmUS_OP_UNARY_BITNOT:{var dst=this["_operand"]();regs[base+dst]=~regs[base+this["_operand"]()];break}case __p_BmUS_OP_TYPEOF:{var dst=this["_operand"]();regs[base+dst]=typeof regs[base+this["_operand"]()];break}case __p_BmUS_OP_VOID:{var dst=this["_operand"]();this["_operand"]();regs[base+dst]=undefined;break}case __p_BmUS_OP_TYPEOF_SAFE:{var dst=this["_operand"]();var name=this["_constant"]();var val=Object["prototype"]["hasOwnProperty"]["call"](this["globals"],name)?this["globals"][name]:undefined;regs[base+dst]=typeof val;break}case __p_BmUS_OP_JUMP:regs[fp+SLOTS["PC"]]=this["_operand"]();break;case __p_BmUS_OP_JUMP_IF_FALSE:{var src=this["_operand"]();var target=this["_operand"]();if(!regs[base+src]){regs[fp+SLOTS["PC"]]=target}break}case __p_BmUS_OP_JUMP_IF_TRUE:{var src=this["_operand"]();var target=this["_operand"]();if(regs[base+src]){regs[fp+SLOTS["PC"]]=target}break}case __p_BmUS_OP_CALL:{var dst=this["_operand"]();var callee=regs[base+this["_operand"]()];var argc=this["_operand"]();var args;if(argc===__p_2AtS_SENTINELS_CALL_SPREAD){args=regs[base+this["_operand"]()]}else{args=new Array(argc);for(var i=0;i>1):0]=retVal;break}case __p_BmUS_OP_THROW:throw regs[base+this["_operand"]()];case __p_BmUS_OP_MAKE_CLOSURE:{var dst=this["_operand"]();var startPc=this["_operand"]();var paramCount=this["_operand"]();var regCount=this["_operand"]();var uvCount=this["_operand"]();var hasRest=this["_operand"]();var uvDescs=new Array(uvCount);for(var i=0;i=iter["_keys"]["length"]){regs[fp+SLOTS["PC"]]=exitTarget}else{regs[base+dst]=iter["_keys"][iter["i"]++]}break}case __p_BmUS_OP_TRY_SETUP:{var hs=regs[fp+SLOTS["HANDLERS"]];if(!hs){regs[fp+SLOTS["HANDLERS"]]=hs=[]}hs["push"]({["handlerPc"]:this["_operand"](),["exceptionReg"]:this["_operand"]()});break}case __p_BmUS_OP_TRY_END:{regs[fp+SLOTS["HANDLERS"]]["pop"]();break}case __p_BmUS_OP_FINALLY_SETUP:{var hs=regs[fp+SLOTS["HANDLERS"]];if(!hs){regs[fp+SLOTS["HANDLERS"]]=hs=[]}hs["push"]({["finallyPc"]:this["_operand"](),["contReg"]:this["_operand"](),["payloadReg"]:this["_operand"](),["throwPad"]:this["_operand"]()});break}case __p_BmUS_OP_PATCH:{var destPc=this["_operand"]();var sliceStart=this["_operand"]();var sliceEnd=this["_operand"]();var pk=this["_operand"]()^destPc|0;for(var pi=sliceStart;pi>>13))>>>0}break}case __p_BmUS_OP_JUMP_REG:{regs[fp+SLOTS["PC"]]=regs[base+this["_operand"]()];break}case __p_BmUS_OP_DEBUGGER:{debugger;break}default:throw new Error("Unknown opcode: "+op+" at pc "+(regs[fp+SLOTS["PC"]]-1))}}catch(err){var handledFrame=0;var searchFrame=this["_f"];while(searchFrame){var handlers=regs[searchFrame+SLOTS["HANDLERS"]];if(handlers&&handlers["length"]>0){handledFrame=searchFrame;break}this["_closeUpvaluesFor"](searchFrame);this["_regsTop"]=searchFrame;searchFrame=regs[searchFrame+SLOTS["CALLER"]];this["_f"]=searchFrame}if(!handledFrame){throw err}var h=regs[handledFrame+SLOTS["HANDLERS"]]["pop"]();var hBase=regs[handledFrame+SLOTS["REG_BASE"]];if(h["exceptionReg"]!==undefined){regs[hBase+h["exceptionReg"]]=err;regs[handledFrame+SLOTS["PC"]]=h["handlerPc"]}else{regs[hBase+h["contReg"]]=h["throwPad"];regs[hBase+h["payloadReg"]]=err;regs[handledFrame+SLOTS["PC"]]=h["finallyPc"]}this["_regsTop"]=handledFrame+regs[handledFrame+SLOTS["FRAME_SIZE"]];this["_f"]=handledFrame}if(!this["_f"]){return this["_regs"][0]}}};var globals=globalThis;if(typeof window!=="undefined"){globals["window"]=window;globals["document"]=typeof document!=="undefined"?document:undefined}if(typeof module!=="undefined"){globals["module"]=module;globals["exports"]=typeof exports!=="undefined"?exports:undefined}var vm=new VM(decodeBytecode(BYTECODE),CONSTANTS,globals);vm["run"](new Closure({["paramCount"]:0,["regCount"]:MAIN_REG_COUNT,["startPc"]:MAIN_START_PC}),undefined,null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/outer-layers/source.js b/test/vm/jsconfuser-vm/fixtures/outer-layers/source.js new file mode 100644 index 00000000..fef7c1f9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/outer-layers/source.js @@ -0,0 +1,7 @@ +function Counter(start) { this.value = start; } +function total(limit) { + var counter = new Counter(0); + for (var i = 0; i < limit; i++) counter.value += i; + return counter.value; +} +window.TEST_OUTPUT = total(4) + Math.max(2, 7) * 100; diff --git a/test/vm/jsconfuser-vm/fixtures/outer-layers/variable-masking.encoded.js b/test/vm/jsconfuser-vm/fixtures/outer-layers/variable-masking.encoded.js new file mode 100644 index 00000000..57df160d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/outer-layers/variable-masking.encoded.js @@ -0,0 +1 @@ +var CONSTANTS=["window","TEST_OUTPUT",4,"Math","max",2,7,100,undefined,"value",0,1];var BYTECODE=[63815,1,3126,4,90,1,4,0,0,51108,2,4,3126,4,106,1,8,1,0,1,2,51108,3,4,9745,4,0,0,58675,5,1,0,58675,6,2,0,31339,7,3,1,6,9745,6,3,0,58675,8,4,0,9119,9,6,8,58675,8,5,0,58675,10,6,0,49178,11,6,9,2,8,10,58675,6,7,0,3343,8,11,6,45047,6,7,8,21956,4,5,6,58675,4,8,0,10097,4,63815,2,58675,3,9,0,21956,2,3,0,58675,3,8,0,10097,3,63815,2,62699,5,0,58675,6,10,0,45538,7,5,1,6,51108,3,7,58675,5,10,0,51108,4,5,6858,5,4,0,62628,5,169,58675,5,9,0,9119,6,3,5,45047,7,6,4,21956,3,5,7,51108,5,4,58675,5,11,0,45047,6,4,5,51108,4,6,1248,130,58675,5,9,0,9119,6,3,5,10097,6,58675,5,8,0,10097,5];var MAIN_START_PC=0;var MAIN_REG_COUNT=12;var ENCODE_BYTECODE=false;var TIMING_CHECKS=false;var OP={["LOAD_CONST"]:58675,["LOAD_INT"]:59513,["LOAD_GLOBAL"]:9745,["LOAD_UPVALUE"]:62699,["LOAD_THIS"]:63815,["MOVE"]:51108,["STORE_GLOBAL"]:58865,["STORE_UPVALUE"]:5169,["GET_PROP"]:9119,["SET_PROP"]:21956,["DELETE_PROP"]:37517,["ADD"]:45047,["SUB"]:58367,["MUL"]:3343,["DIV"]:15331,["MOD"]:37910,["EXP"]:54744,["BAND"]:61072,["BOR"]:22373,["BXOR"]:49895,["SHL"]:24807,["SHR"]:39480,["USHR"]:1383,["LT"]:6858,["GT"]:50789,["LTE"]:49605,["GTE"]:62024,["EQ"]:37588,["NEQ"]:54099,["LOOSE_EQ"]:9938,["LOOSE_NEQ"]:31693,["IN"]:58249,["INSTANCEOF"]:55198,["UNARY_NEG"]:24245,["UNARY_POS"]:30173,["UNARY_NOT"]:24355,["UNARY_BITNOT"]:2075,["TYPEOF"]:42683,["VOID"]:2210,["TYPEOF_SAFE"]:31433,["JUMP"]:1248,["JUMP_IF_FALSE"]:62628,["JUMP_IF_TRUE"]:51402,["CALL"]:31339,["CALL_METHOD"]:49178,["NEW"]:45538,["RETURN"]:10097,["THROW"]:31958,["MAKE_CLOSURE"]:3126,["BUILD_ARRAY"]:32390,["BUILD_OBJECT"]:48733,["DEFINE_GETTER"]:31933,["DEFINE_SETTER"]:60588,["FOR_IN_SETUP"]:45797,["FOR_IN_NEXT"]:32563,["TRY_SETUP"]:2373,["TRY_END"]:56388,["PATCH"]:43666,["DEBUGGER"]:30171,["JUMP_REG"]:30352,["FINALLY_SETUP"]:4416};var SENTINELS={["CALL_SPREAD"]:1638052255};var SLOTS={["NOISE_END"]:6,["REG_BASE"]:9,["RET_DST"]:0,["THIS"]:3,["HANDLERS"]:11,["NOISE_PARAMS"]:10,["FRAME_SIZE"]:1,["CALLER"]:8,["PC"]:2,["CLOSURE"]:4};var HEADER_SIZE=13;var FRAME_START=16;function base64ToBytes(...__p_y52G_varMask){return typeof Buffer!=="undefined"?Buffer["from"](__p_y52G_varMask[0],"base64"):Uint8Array["from"](atob(__p_y52G_varMask[0]),function(...__p_BmUS_varMask){__p_BmUS_varMask["length"]=1;return __p_BmUS_varMask[0]["charCodeAt"](0)})}function decodeBytecode(...__p_1UFd_varMask){if(!ENCODE_BYTECODE){return __p_1UFd_varMask[0]}__p_1UFd_varMask[1]=base64ToBytes(__p_1UFd_varMask[0]);__p_1UFd_varMask["b"]=new Uint32Array(__p_1UFd_varMask[1]["length"]/4);for(__p_1UFd_varMask["c"]=0;__p_1UFd_varMask["c"]<__p_1UFd_varMask["b"]["length"];__p_1UFd_varMask["c"]++){__p_1UFd_varMask["b"][__p_1UFd_varMask["c"]]=(__p_1UFd_varMask[1][__p_1UFd_varMask["c"]*4]|__p_1UFd_varMask[1][__p_1UFd_varMask["c"]*4+1]<<8|__p_1UFd_varMask[1][__p_1UFd_varMask["c"]*4+2]<<16|__p_1UFd_varMask[1][__p_1UFd_varMask["c"]*4+3]<<24)>>>0}return __p_1UFd_varMask["b"]}var CLOSURE_MAP=new WeakMap;function Upvalue(regs,absSlot){this["_regs"]=regs;this["_absSlot"]=absSlot;this["_closed"]=false;this["_value"]=undefined}Upvalue["prototype"]["_read"]=function(){return this["_closed"]?this["_value"]:this["_regs"][this["_absSlot"]]};Upvalue["prototype"]["_write"]=function(v){if(this["_closed"]){this["_value"]=v}else{this["_regs"][this["_absSlot"]]=v}};Upvalue["prototype"]["_close"]=function(){this["_value"]=this["_regs"][this["_absSlot"]];this["_closed"]=true};function Closure(fn){this["fn"]=fn;this["upvalues"]=[];this["prototype"]={}}function VM(bytecode,constants,globals){this["bytecode"]=bytecode;this["constants"]=constants;this["globals"]=globals;this["_openUpvalues"]=null;this["_regs"]=[];this["_regsTop"]=FRAME_START;this["_f"]=0}VM["prototype"]["_operand"]=function(){return this["bytecode"][this["_regs"][this["_f"]+SLOTS["PC"]]++]};VM["prototype"]["_pushFrame"]=function(closure,args,thisVal,retDst){var fn=closure["fn"];var regs=this["_regs"];var fp=this["_regsTop"];var size=HEADER_SIZE+fn["regCount"];var end=fp+size;while(regs["length"]>>13)&65535;var code=b[i*2]|b[i*2+1]<<8;out+=String["fromCharCode"](code^ks)}return out};VM["prototype"]["_closeUpvaluesFor"]=function(fp){var uvs=this["_openUpvalues"];if(!uvs){return}var regs=this["_regs"];var hi=fp+regs[fp+SLOTS["FRAME_SIZE"]];for(var i=regs[fp+SLOTS["REG_BASE"]];i{return performance["now"]()};var lastTime=now();this["_pushFrame"](closure,args,thisVal,0);while(true){var fp=this["_f"];var regs=this["_regs"];var bc=this["bytecode"];var pc=regs[fp+SLOTS["PC"]];if(pc>=bc["length"]){break}regs[fp+SLOTS["PC"]]=pc+1;var op=this["bytecode"][pc];if(typeof SLOTS["NOISE_PC"]==="number"){regs[fp+SLOTS["NOISE_PC"]]=(regs[fp+SLOTS["NOISE_PC"]]+1)%bc["length"]}if(typeof SLOTS["NOISE_COUNTER"]==="number"){regs[fp+SLOTS["NOISE_COUNTER"]]++}if(typeof SLOTS["NOISE_MIRROR"]==="number"){regs[fp+SLOTS["NOISE_MIRROR"]]=op}if(typeof SLOTS["NOISE_ACC"]==="number"){regs[fp+SLOTS["NOISE_ACC"]]=regs[fp+SLOTS["NOISE_ACC"]]^pc|0}if(TIMING_CHECKS){var currentTime=now();var isTamper=currentTime-lastTime>TIMING_CHECKS;lastTime=currentTime;if(isTamper){for(var i=0;i>regs[base+this["_operand"]()];break}case OP["USHR"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a>>>regs[base+this["_operand"]()];break}case OP["LT"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=aregs[base+this["_operand"]()];break}case OP["LTE"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a<=regs[base+this["_operand"]()];break}case OP["GTE"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a>=regs[base+this["_operand"]()];break}case OP["EQ"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a===regs[base+this["_operand"]()];break}case OP["NEQ"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a!==regs[base+this["_operand"]()];break}case OP["LOOSE_EQ"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a==regs[base+this["_operand"]()];break}case OP["LOOSE_NEQ"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a!=regs[base+this["_operand"]()];break}case OP["IN"]:{var dst=this["_operand"]();var a=regs[base+this["_operand"]()];regs[base+dst]=a in regs[base+this["_operand"]()];break}case OP["INSTANCEOF"]:{var dst=this["_operand"]();var obj=regs[base+this["_operand"]()];regs[base+dst]=obj instanceof regs[base+this["_operand"]()];break}case OP["UNARY_NEG"]:{var dst=this["_operand"]();regs[base+dst]=-regs[base+this["_operand"]()];break}case OP["UNARY_POS"]:{var dst=this["_operand"]();regs[base+dst]=+regs[base+this["_operand"]()];break}case OP["UNARY_NOT"]:{var dst=this["_operand"]();regs[base+dst]=!regs[base+this["_operand"]()];break}case OP["UNARY_BITNOT"]:{var dst=this["_operand"]();regs[base+dst]=~regs[base+this["_operand"]()];break}case OP["TYPEOF"]:{var dst=this["_operand"]();regs[base+dst]=typeof regs[base+this["_operand"]()];break}case OP["VOID"]:{var dst=this["_operand"]();this["_operand"]();regs[base+dst]=undefined;break}case OP["TYPEOF_SAFE"]:{var dst=this["_operand"]();var name=this["_constant"]();var val=Object["prototype"]["hasOwnProperty"]["call"](this["globals"],name)?this["globals"][name]:undefined;regs[base+dst]=typeof val;break}case OP["JUMP"]:regs[fp+SLOTS["PC"]]=this["_operand"]();break;case OP["JUMP_IF_FALSE"]:{var src=this["_operand"]();var target=this["_operand"]();if(!regs[base+src]){regs[fp+SLOTS["PC"]]=target}break}case OP["JUMP_IF_TRUE"]:{var src=this["_operand"]();var target=this["_operand"]();if(regs[base+src]){regs[fp+SLOTS["PC"]]=target}break}case OP["CALL"]:{var dst=this["_operand"]();var callee=regs[base+this["_operand"]()];var argc=this["_operand"]();var args;if(argc===SENTINELS["CALL_SPREAD"]){args=regs[base+this["_operand"]()]}else{args=new Array(argc);for(var i=0;i>1):0]=retVal;break}case OP["THROW"]:throw regs[base+this["_operand"]()];case OP["MAKE_CLOSURE"]:{var dst=this["_operand"]();var startPc=this["_operand"]();var paramCount=this["_operand"]();var regCount=this["_operand"]();var uvCount=this["_operand"]();var hasRest=this["_operand"]();var uvDescs=new Array(uvCount);for(var i=0;i=iter["_keys"]["length"]){regs[fp+SLOTS["PC"]]=exitTarget}else{regs[base+dst]=iter["_keys"][iter["i"]++]}break}case OP["TRY_SETUP"]:{var hs=regs[fp+SLOTS["HANDLERS"]];if(!hs){regs[fp+SLOTS["HANDLERS"]]=hs=[]}hs["push"]({["handlerPc"]:this["_operand"](),["exceptionReg"]:this["_operand"]()});break}case OP["TRY_END"]:{regs[fp+SLOTS["HANDLERS"]]["pop"]();break}case OP["FINALLY_SETUP"]:{var hs=regs[fp+SLOTS["HANDLERS"]];if(!hs){regs[fp+SLOTS["HANDLERS"]]=hs=[]}hs["push"]({["finallyPc"]:this["_operand"](),["contReg"]:this["_operand"](),["payloadReg"]:this["_operand"](),["throwPad"]:this["_operand"]()});break}case OP["PATCH"]:{var destPc=this["_operand"]();var sliceStart=this["_operand"]();var sliceEnd=this["_operand"]();var pk=this["_operand"]()^destPc|0;for(var pi=sliceStart;pi>>13))>>>0}break}case OP["JUMP_REG"]:{regs[fp+SLOTS["PC"]]=regs[base+this["_operand"]()];break}case OP["DEBUGGER"]:{debugger;break}default:throw new Error("Unknown opcode: "+op+" at pc "+(regs[fp+SLOTS["PC"]]-1))}}catch(err){var handledFrame=0;var searchFrame=this["_f"];while(searchFrame){var handlers=regs[searchFrame+SLOTS["HANDLERS"]];if(handlers&&handlers["length"]>0){handledFrame=searchFrame;break}this["_closeUpvaluesFor"](searchFrame);this["_regsTop"]=searchFrame;searchFrame=regs[searchFrame+SLOTS["CALLER"]];this["_f"]=searchFrame}if(!handledFrame){throw err}var h=regs[handledFrame+SLOTS["HANDLERS"]]["pop"]();var hBase=regs[handledFrame+SLOTS["REG_BASE"]];if(h["exceptionReg"]!==undefined){regs[hBase+h["exceptionReg"]]=err;regs[handledFrame+SLOTS["PC"]]=h["handlerPc"]}else{regs[hBase+h["contReg"]]=h["throwPad"];regs[hBase+h["payloadReg"]]=err;regs[handledFrame+SLOTS["PC"]]=h["finallyPc"]}this["_regsTop"]=handledFrame+regs[handledFrame+SLOTS["FRAME_SIZE"]];this["_f"]=handledFrame}if(!this["_f"]){return this["_regs"][0]}}};var globals=globalThis;if(typeof window!=="undefined"){globals["window"]=window;globals["document"]=typeof document!=="undefined"?document:undefined}if(typeof module!=="undefined"){globals["module"]=module;globals["exports"]=typeof exports!=="undefined"?exports:undefined}var vm=new VM(decodeBytecode(BYTECODE),CONSTANTS,globals);vm["run"](new Closure({["paramCount"]:0,["regCount"]:MAIN_REG_COUNT,["startPc"]:MAIN_START_PC}),undefined,null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/outer-layers/vm.encoded.js b/test/vm/jsconfuser-vm/fixtures/outer-layers/vm.encoded.js new file mode 100644 index 00000000..6eda0775 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/outer-layers/vm.encoded.js @@ -0,0 +1,1021 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */4, /* 3 */"Math", /* 4 */"max", /* 5 */2, /* 6 */7, /* 7 */100, /* 8 */undefined, /* 9 */"value", /* 10 */0, /* 11 */1]; +var BYTECODE = [63815, 1, 3126, 4, 90, 1, 4, 0, 0, 51108, 2, 4, 3126, 4, 106, 1, 8, 1, 0, 1, 2, 51108, 3, 4, 9745, 4, 0, 0, 58675, 5, 1, 0, 58675, 6, 2, 0, 31339, 7, 3, 1, 6, 9745, 6, 3, 0, 58675, 8, 4, 0, 9119, 9, 6, 8, 58675, 8, 5, 0, 58675, 10, 6, 0, 49178, 11, 6, 9, 2, 8, 10, 58675, 6, 7, 0, 3343, 8, 11, 6, 45047, 6, 7, 8, 21956, 4, 5, 6, 58675, 4, 8, 0, 10097, 4, 63815, 2, 58675, 3, 9, 0, 21956, 2, 3, 0, 58675, 3, 8, 0, 10097, 3, 63815, 2, 62699, 5, 0, 58675, 6, 10, 0, 45538, 7, 5, 1, 6, 51108, 3, 7, 58675, 5, 10, 0, 51108, 4, 5, 6858, 5, 4, 0, 62628, 5, 169, 58675, 5, 9, 0, 9119, 6, 3, 5, 45047, 7, 6, 4, 21956, 3, 5, 7, 51108, 5, 4, 58675, 5, 11, 0, 45047, 6, 4, 5, 51108, 4, 6, 1248, 130, 58675, 5, 9, 0, 9119, 6, 3, 5, 10097, 6, 58675, 5, 8, 0, 10097, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 58675, + LOAD_INT: 59513, + LOAD_GLOBAL: 9745, + LOAD_UPVALUE: 62699, + LOAD_THIS: 63815, + MOVE: 51108, + STORE_GLOBAL: 58865, + STORE_UPVALUE: 5169, + GET_PROP: 9119, + SET_PROP: 21956, + DELETE_PROP: 37517, + ADD: 45047, + SUB: 58367, + MUL: 3343, + DIV: 15331, + MOD: 37910, + EXP: 54744, + BAND: 61072, + BOR: 22373, + BXOR: 49895, + SHL: 24807, + SHR: 39480, + USHR: 1383, + LT: 6858, + GT: 50789, + LTE: 49605, + GTE: 62024, + EQ: 37588, + NEQ: 54099, + LOOSE_EQ: 9938, + LOOSE_NEQ: 31693, + IN: 58249, + INSTANCEOF: 55198, + UNARY_NEG: 24245, + UNARY_POS: 30173, + UNARY_NOT: 24355, + UNARY_BITNOT: 2075, + TYPEOF: 42683, + VOID: 2210, + TYPEOF_SAFE: 31433, + JUMP: 1248, + JUMP_IF_FALSE: 62628, + JUMP_IF_TRUE: 51402, + CALL: 31339, + CALL_METHOD: 49178, + NEW: 45538, + RETURN: 10097, + THROW: 31958, + MAKE_CLOSURE: 3126, + BUILD_ARRAY: 32390, + BUILD_OBJECT: 48733, + DEFINE_GETTER: 31933, + DEFINE_SETTER: 60588, + FOR_IN_SETUP: 45797, + FOR_IN_NEXT: 32563, + TRY_SETUP: 2373, + TRY_END: 56388, + PATCH: 43666, + DEBUGGER: 30171, + JUMP_REG: 30352, + FINALLY_SETUP: 4416 +}; +var SENTINELS = { + CALL_SPREAD: 1638052255 +}; +var SLOTS = { + NOISE_END: 6, + REG_BASE: 9, + RET_DST: 0, + THIS: 3, + HANDLERS: 11, + NOISE_PARAMS: 10, + FRAME_SIZE: 1, + CALLER: 8, + PC: 2, + CLOSURE: 4 +}; +var HEADER_SIZE = 13; +var FRAME_START = 16; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); diff --git a/test/vm/jsconfuser-vm/fixtures/reference/encoded.debug.js b/test/vm/jsconfuser-vm/fixtures/reference/encoded.debug.js new file mode 100644 index 00000000..ea2031e9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/reference/encoded.debug.js @@ -0,0 +1,1029 @@ +// fn_0_0: +// [4, 1], LOAD_THIS reg[1] = this +// [2, 2, 0, 0], LOAD_GLOBAL reg[2] = window 1:0-1:6 +// [0, 3, 1, 0], LOAD_CONST reg[3] = "TEST_OUTPUT" 1:0-1:26 +// [0, 4, 2, 0], LOAD_CONST reg[4] = 1 1:21-1:22 +// [0, 5, 3, 0], LOAD_CONST reg[5] = 2 1:25-1:26 +// [11, 6, 4, 5], ADD reg[6] = reg[4] + reg[5] 1:21-1:26 +// [9, 2, 3, 6], SET_PROP reg[2][reg[3]] = reg[6] 1:0-1:26 +// [0, 2, 4, 0], LOAD_CONST reg[2] = undefined +// [45, 2], RETURN reg[2] +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */2, /* 4 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 11, 6, 4, 5, 9, 2, 3, 6, 0, 2, 4, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/reference/encoded.js b/test/vm/jsconfuser-vm/fixtures/reference/encoded.js new file mode 100644 index 00000000..50946c7a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/reference/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */2, /* 4 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 11, 6, 4, 5, 9, 2, 3, 6, 0, 2, 4, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/reference/oracle.json b/test/vm/jsconfuser-vm/fixtures/reference/oracle.json new file mode 100644 index 00000000..7d12cbbc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/reference/oracle.json @@ -0,0 +1,20 @@ +{ + "expected": 3, + "timeoutMs": 2000, + "boundary": "child_process.spawnSync with finite timeout and SIGKILL kill signal", + "source": { + "ok": true, + "value": 3, + "timedOut": false + }, + "raw": { + "ok": true, + "value": 3, + "timedOut": false + }, + "stripped": { + "ok": true, + "value": 3, + "timedOut": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/reference/source.js b/test/vm/jsconfuser-vm/fixtures/reference/source.js new file mode 100644 index 00000000..6fb44479 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/reference/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = 1 + 2; diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-aliased.encoded.js b/test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-aliased.encoded.js new file mode 100644 index 00000000..d7cdb49f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-aliased.encoded.js @@ -0,0 +1,18 @@ +var c=new WeakMap;function ca(a,h){this.g=h;this.o=a;this.u=!1;this.t={};this.y=void 0}function A(a,h,b){this.z=b;this.e=h;this.i=a;this.v=null;this.g=[];this.Q=1;this.h=0}function da(a,h){this.J=a;this.j=[];this.prototype={};this.f=h}function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(h){return h.charCodeAt(0)})}function H(a){return a.z[a.g[a.h+0]++]} +A.prototype.r=function(a,h,b){a=this.g[a+7]+h;h=this.v||(this.v=[]);this.b=b;return h[a]||(h[a]=new ca(a,this.g))};function ua(a){return a.u?a.y:a.g[a.o]}ca.prototype.s=function(a,h,b,f){this.w=[f,a];this.u?this.y=b:this.g[this.o]=b};function I(a,h,b){a.n=h;if(h=a.v){var f=a.g;a=b+f[b+6];for(b=f[b+7];b>>13)&65535);return h} +A.prototype.E=function(a,h,b,f,n,g,l){f=b.J;g=this.g;for(var d=this.Q,e=8+f.m,q=d+e;g.length=a.z.length)break;b[f+0]=l+1;l=a.z[l];try{var d=b[f+7];switch(l){case 0:var e=H(a);b[d+e]=J(a);break;case 1:e=H(a);b[d+e]=H(a);break;case 2:e=H(a);var q=J(a);if(!(q in a.i))throw new ReferenceError(`${q} is not defined`);b[d+e]=a.i[q];break;case 3:e=H(a);b[d+e]=ua(b[f+4].j[H(a)]);break;case 4:e=H(a);b[d+e]=b[f+3];break;case 5:e=H(a);b[d+e]=b[d+H(a)];break;case 6:a.i[J(a)]=b[d+H(a)];break;case 7:var r= +H(a);b[f+4].j[r].s(void 0,void 0,b[d+H(a)],void 0);break;case 8:e=H(a);var m=b[d+H(a)],p=b[d+H(a)];b[d+e]=m[p];break;case 9:m=b[d+H(a)];p=b[d+H(a)];var w=b[d+H(a)];Reflect.set(m,p,w);break;case 10:e=H(a);m=b[d+H(a)];p=b[d+H(a)];b[d+e]=delete m[p];break;case 11:e=H(a);var k=b[d+H(a)];b[d+e]=k+b[d+H(a)];break;case 12:e=H(a);k=b[d+H(a)];b[d+e]=k-b[d+H(a)];break;case 13:e=H(a);k=b[d+H(a)];b[d+e]=k*b[d+H(a)];break;case 14:e=H(a);k=b[d+H(a)];b[d+e]=k/b[d+H(a)];break;case 15:e=H(a);k=b[d+H(a)];b[d+e]=k% +b[d+H(a)];break;case 60:e=H(a);k=b[d+H(a)];b[d+e]=Math.pow(k,b[d+H(a)]);break;case 16:e=H(a);k=b[d+H(a)];b[d+e]=k&b[d+H(a)];break;case 17:e=H(a);k=b[d+H(a)];b[d+e]=k|b[d+H(a)];break;case 18:e=H(a);k=b[d+H(a)];b[d+e]=k^b[d+H(a)];break;case 19:e=H(a);k=b[d+H(a)];b[d+e]=k<>b[d+H(a)];break;case 21:e=H(a);k=b[d+H(a)];b[d+e]=k>>>b[d+H(a)];break;case 22:e=H(a);k=b[d+H(a)];b[d+e]=kb[d+H(a)];break;case 24:e= +H(a);k=b[d+H(a)];b[d+e]=k<=b[d+H(a)];break;case 25:e=H(a);k=b[d+H(a)];b[d+e]=k>=b[d+H(a)];break;case 26:e=H(a);k=b[d+H(a)];b[d+e]=k===b[d+H(a)];break;case 27:e=H(a);k=b[d+H(a)];b[d+e]=k!==b[d+H(a)];break;case 28:e=H(a);k=b[d+H(a)];b[d+e]=k==b[d+H(a)];break;case 29:e=H(a);k=b[d+H(a)];b[d+e]=k!=b[d+H(a)];break;case 30:e=H(a);k=b[d+H(a)];b[d+e]=k in b[d+H(a)];break;case 31:e=H(a);m=b[d+H(a)];b[d+e]=m instanceof b[d+H(a)];break;case 32:e=H(a);b[d+e]=-b[d+H(a)];break;case 33:e=H(a);b[d+e]=+b[d+H(a)];break; +case 34:e=H(a);b[d+e]=!b[d+H(a)];break;case 35:e=H(a);b[d+e]=~b[d+H(a)];break;case 36:e=H(a);b[d+e]=typeof b[d+H(a)];break;case 37:e=H(a);H(a);b[d+e]=void 0;break;case 38:e=H(a);var fa=J(a);w=Object.prototype.hasOwnProperty.call(a.i,fa)?a.i[fa]:void 0;b[d+e]=typeof w;break;case 39:b[f+0]=H(a);break;case 40:var D=H(a),E=H(a);b[d+D]||(b[f+0]=E);break;case 41:D=H(a);E=H(a);b[d+D]&&(b[f+0]=E);break;case 42:e=H(a);var t=b[d+H(a)],u=H(a);if(u===65535)h=b[d+H(a)];else for(h=Array(u),g=0;g>1):0]=v;break;case 46:throw b[d+H(a)];case 47:e=H(a);var wa=H(a),xa=H(a),ya=H(a),ia=H(a),za=H(a),Q=Array(ia);for(g=0;g=M.C.length?b[f+0]=Ga:b[d+e]=M.C[M.B++];break;case 54:var B=b[f+5];B||(b[f+5]=B=[]);B.push({G:H(a),A:H(a)});break;case 55:b[f+5].pop();break;case 59:(B=b[f+5])||(b[f+5]=B=[]);B.push({x:H(a),F:H(a),H:H(a),k:H(a)});break;case 56:var oa=H(a),pa=H(a),Ha=H(a),N=H(a)^oa|0;for(f=pa;f>>13)>>>0;break;case 58:b[f+0]=b[d+H(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+ +l+" at pc "+(b[f+0]-1));case 61:let z=[H(a),H(a),H(a)],T=[z[1],z[2],z[0]];e=T[0];b[d+e]=J(a,97,2,T[2],T[1]);break;case 62:let qa=[H(a),H(a)],ra=[qa[1],qa[0]];e=ra[0];b[d+e]=b[d+ra[1]];break;case 63:let Ia=[H(a)];v=b[d+Ia[0]];I(a,void 0,f);x=b[f+1];F=b[f+2];F&1&&(typeof v!=="object"||v===null)&&(v=b[f+3]);P=f+b[f+6];for(g=f;g>1):0]=v;break;case 64:e=H(a);b[d+e]=b[f+3];break;case 65:let U=[H(a),H(a),H(a)],V=[U[0],U[2],U[1]];e=V[0];k=b[d+V[1]];b[d+e]=k+b[d+ +V[2]];break;case 66:let W=[H(a),H(a),H(a)],X=[W[0],W[2],W[1]];e=X[0];q=J(a,90,47,X[2],X[1]);if(!(q in a.i))throw new ReferenceError(`${q} is not defined`);b[d+e]=a.i[q];break;case 67:let Y=[H(a),H(a),H(a)],Z=[Y[1],Y[2],Y[0]];m=b[d+Z[0]];p=b[d+Z[1]];w=b[d+Z[2]];Reflect.set(m,p,w);break;case 68:let aa=[H(a),H(a),H(a)],ba=[aa[2],aa[0],aa[1]];e=ba[0];k=b[d+ba[1]];b[d+e]=k0){f=l;break}I(a,{},l);a.Q=l;l=b[l+1];a.h=l}if(!f)throw z;l=b[f+5].pop();C=b[f+7];l.A!==void 0?(b[C+l.A]=z,b[f+0]=l.G):(b[C+l.F]=l.k,b[C+l.H]=z,b[f+0]=l.x);a.Q=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var O=globalThis;typeof window!=="undefined"&&(O.window=window,O.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(O.module=module,O.exports=typeof exports!=="undefined"?exports:void 0); +va(new A(O,["window","TEST_OUTPUT",4,void 0,0,1],[64,1,47,3,39,1,7,0,0,62,3,2,66,3,0,0,61,0,4,1,61,0,5,2,42,6,2,1,5,67,6,3,4,61,0,3,3,63,3,64,2,61,0,5,4,62,5,3,61,0,5,4,62,5,4,68,4,0,5,69,85,5,65,5,4,3,62,5,3,62,4,5,61,0,5,5,65,6,5,4,62,6,4,70,55,63,3,61,0,5,3,63,5]),null,void 0,void 0,new da({l:0,m:7,D:0},30)); diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-specialized.encoded.js b/test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-specialized.encoded.js new file mode 100644 index 00000000..719aa59d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-specialized.encoded.js @@ -0,0 +1,20 @@ +function w(a,b,g,e){this.l=e;this.t=b;this.i=g;this.b=null;this.h=[];this.a=a;this.j=1;this.g=0}function ca(a,b){this.h=b;this.m=a;this.f=!1;this.o=void 0}var A=new WeakMap;function da(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}function H(a,b){this.z=b;this.y=[];this.prototype={};this.Q=a}function ea(a,b){a.f?a.o=b:a.h[a.m]=b}w.prototype.s=function(){return this.l[this.h[this.g+0]++]}; +w.prototype.w=function(a,b,g){a=this.h[b+7]+g;b=this.b||(this.b=[]);return b[a]||(b[a]=new ca(a,this.h))};function I(a,b,g,e){b=b??a.s(void 0);e=e??a.s([]);a.E=g;a=a.t[b];if(!e)return a;if(typeof a==="number")return a^e;if(typeof a!=="string")return a;a=da(a);g="";for(b=0;b>>13)&65535);return g}function fa(a){return a.f?a.o:a.h[a.m]} +w.prototype.c=function(a,b,g,e){if(a=this.b)for(g=this.h,b=e+g[e+6],e=g[e+7];e=a.l.length)break;b[e+0]=k+1;k=a.l[k];try{var c=b[e+7];switch(k){case 0:var d=a.s(void 0);b[c+d]=I(a);break;case 1:d=a.s(41);b[c+d]=a.s(84);break;case 2:d=a.s(98);var p=I(a);if(!(p in a.i))throw new ReferenceError(`${p} is not defined`);b[c+d]=a.i[p];break;case 3:d=a.s([]);b[c+d]=fa(b[e+4].y[a.s({})]);break;case 4:d=a.s(void 0);b[c+d]=b[e+3];break;case 5:d=a.s({});b[c+d]=b[c+a.s(void 0)];break;case 6:a.i[I(a)]= +b[c+a.s(void 0)];break;case 7:var ha=a.s("y");ea(b[e+4].y[ha],b[c+a.s([])]);break;case 8:d=a.s("x");var l=b[c+a.s([])],q=b[c+a.s(void 0)];b[c+d]=l[q];break;case 9:l=b[c+a.s(null)];q=b[c+a.s("x")];var x=b[c+a.s("k")];Reflect.set(l,q,x);break;case 10:d=a.s({});l=b[c+a.s("z")];q=b[c+a.s(void 0)];b[c+d]=delete l[q];break;case 11:d=a.s(null);var h=b[c+a.s(51)];b[c+d]=h+b[c+a.s({})];break;case 12:d=a.s(null);h=b[c+a.s(22)];b[c+d]=h-b[c+a.s("z")];break;case 13:d=a.s("z");h=b[c+a.s([])];b[c+d]=h*b[c+a.s([])]; +break;case 14:d=a.s(null);h=b[c+a.s("z")];b[c+d]=h/b[c+a.s([])];break;case 15:d=a.s({});h=b[c+a.s([])];b[c+d]=h%b[c+a.s(void 0)];break;case 60:d=a.s([]);h=b[c+a.s(null)];b[c+d]=Math.pow(h,b[c+a.s([])]);break;case 16:d=a.s(void 0);h=b[c+a.s([])];b[c+d]=h&b[c+a.s([])];break;case 17:d=a.s(void 0);h=b[c+a.s(null)];b[c+d]=h|b[c+a.s(40)];break;case 18:d=a.s(void 0);h=b[c+a.s(null)];b[c+d]=h^b[c+a.s(42)];break;case 19:d=a.s(null);h=b[c+a.s(null)];b[c+d]=h<>b[c+a.s({})];break;case 21:d=a.s([]);h=b[c+a.s(null)];b[c+d]=h>>>b[c+a.s([])];break;case 22:d=a.s([]);h=b[c+a.s(null)];b[c+d]=hb[c+a.s(void 0)];break;case 24:d=a.s(void 0);h=b[c+a.s("q")];b[c+d]=h<=b[c+a.s(85)];break;case 25:d=a.s({});h=b[c+a.s(void 0)];b[c+d]=h>=b[c+a.s(void 0)];break;case 26:d=a.s(null);h=b[c+a.s({})];b[c+d]=h===b[c+a.s("x")];break;case 27:d=a.s(36);h=b[c+a.s(null)];b[c+d]=h!==b[c+a.s("q")];break;case 28:d= +a.s({});h=b[c+a.s(void 0)];b[c+d]=h==b[c+a.s({})];break;case 29:d=a.s(null);h=b[c+a.s({})];b[c+d]=h!=b[c+a.s([])];break;case 30:d=a.s({});h=b[c+a.s(null)];b[c+d]=h in b[c+a.s({})];break;case 31:d=a.s("z");l=b[c+a.s("x")];b[c+d]=l instanceof b[c+a.s(51)];break;case 32:d=a.s(null);b[c+d]=-b[c+a.s({})];break;case 33:d=a.s("q");b[c+d]=+b[c+a.s([])];break;case 34:d=a.s({});b[c+d]=!b[c+a.s(82)];break;case 35:d=a.s("q");b[c+d]=~b[c+a.s("z")];break;case 36:d=a.s(void 0);b[c+d]=typeof b[c+a.s({})];break;case 37:d= +a.s(16);a.s("x");b[c+d]=void 0;break;case 38:d=a.s(void 0);var S=I(a);x=Object.prototype.hasOwnProperty.call(a.i,S)?a.i[S]:void 0;b[c+d]=typeof x;break;case 39:b[e+0]=a.s(null);break;case 40:var D=a.s([]),E=a.s({});b[c+D]||(b[e+0]=E);break;case 41:D=a.s(93);E=a.s(43);b[c+D]&&(b[e+0]=E);break;case 42:d=a.s("q");var t=b[c+a.s(6)],u=a.s("q");if(u===65535)n=b[c+a.s(47)];else for(n=Array(u),f=0;f>1):0]=m;break;case 46:throw b[c+a.s(76)];case 47:d=a.s([]);var ia=a.s("q"),ja=a.s(void 0),ka=a.s({}),U=a.s("q"),la=a.s({}),N=Array(U);for(f=0;f=L.u.length?b[e+0]=ra:b[c+d]=L.u[L.n++];break;case 54:var B=b[e+5];B||(b[e+5]=B=[]);B.push({F:a.s(35),B:a.s("x")});break;case 55:b[e+5].pop();break;case 59:(B=b[e+5])||(b[e+5]=B=[]);B.push({H:a.s({}),p:a.s([]),C:a.s(null),D:a.s(void 0)});break;case 56:var aa=a.s(null),ba=a.s({}),sa=a.s({}),M=a.s({})^aa|0;for(e=ba;e>>13)>>>0;break;case 58:b[e+0]=b[c+a.s(null)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+k+" at pc "+(b[e+0]-1));case 61:d=1;b[c+d]=b[e+3];break;case 62:d=2;b[c+d]=b[c+3];break;case 63:d=3;p=I(a,0,19,0);if(!(p in a.i))throw new ReferenceError(`${p} is not defined`);b[c+d]=a.i[p];break;case 64:d=4;b[c+d]=I(a,1,66,0);break;case 65:d=5;b[c+d]=I(a,2,[],0);break;case 66:l=b[c+3];q=b[c+4];x=b[c+6];Reflect.set(l,q,x);break;case 67:d=3;b[c+d]=I(a,3,null, +0);break;case 68:m=b[c+3];a.c({},43,"y",e);r=b[e+1];v=b[e+2];v&1&&(typeof m!=="object"||m===null)&&(m=b[e+3]);y=e+b[e+6];for(f=e;f>1):0]=m;break;case 69:d=2;b[c+d]=b[e+3];break;case 70:d=5;b[c+d]=I(a,4,"k",0);break;case 71:d=3;b[c+d]=b[c+5];break;case 72:d=4;b[c+d]=b[c+5];break;case 73:d=5;h=b[c+4];b[c+d]=h>1):0]=m;break;case 80:m=b[c+5];a.c(53,null,{},e);r=b[e+1];v=b[e+2];v&1&&(typeof m!=="object"||m===null)&&(m=b[e+3]);y=e+b[e+6];for(f=e;f>1):0]=m}}catch(F){e=0;for(k=a.g;k;){var C=b[k+5];if(C&&C.length>0){e=k;break}a.c(36,void 0,void 0,k);a.j= +k;k=b[k+1];a.g=k}if(!e)throw F;k=b[e+5].pop();C=b[e+7];k.B!==void 0?(b[C+k.B]=F,b[e+0]=k.F):(b[C+k.p]=k.D,b[C+k.C]=F,b[e+0]=k.H);a.j=e+b[e+6];a.g=e}if(!a.g)return a.h[0]}}var R=globalThis;typeof window!=="undefined"&&(R.window=window,R.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(R.module=module,R.exports=typeof exports!=="undefined"?exports:void 0); +Q(new w(null,["window","TEST_OUTPUT",4,void 0,0,1],R,[61,47,3,20,1,7,0,0,62,63,64,65,42,6,2,1,5,66,67,68,69,70,71,0,5,4,0,72,73,74,75,76,5,5,4,0,5,5,0,77,5,4,6,78,79,0,5,3,0,80]),void 0,new H(void 0,{q:0,r:7,v:0}),[],null,"z"); diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-table-shuffle.encoded.js b/test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-table-shuffle.encoded.js new file mode 100644 index 00000000..045091c6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-layers/class-minify-table-shuffle.encoded.js @@ -0,0 +1,21 @@ +function d(a,b,c){this.i=a;this.b=c;this.h=b;this.A=null;this.z=[];this.p=1;this.g=0}function g(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}function q(a,b){this.E=a;this.j=[];this.prototype={};this.d=b}var r=new WeakMap;function t(a,b){this.z=a;this.o=b;this.k="y";this.q=[];this.v=!1;this.u=void 0}function u(a,b,c){b=a.z[b+7]+c;c=a.A||(a.A=[]);a.r=null;a.s=null;return c[b]||(c[b]=new t(a.z,b))} +function v(a,b,c,e,f){for(var h=e.E,k=a.z,l=a.p,p=8+h.l,n=l+p;k.length>>13)&65535);return b}function y(a){return a.i[a.z[a.g+0]++]}d.prototype.f=function(a,b){var c=this.A;if(c){var e=this.z,f=a+e[a+6];this.c=b;for(a=e[a+7];a=a.i.length)break;b[c+0]=e+1;c=a.i[e];try{a[c]()}catch(h){c=0;for(e=a.g;e;){var f=b[e+5];if(f&&f.length>0){c=e;break}a.f(e,{},void 0,null,"q");a.p=e;e=b[e+1];a.g=e}if(!c)throw h;e=b[c+5].pop();f=b[c+7];e.C!==void 0?(b[f+e.C]=h,b[c+0]=e.H):(b[f+e.Q]=e.K,b[f+e.J]=h,b[c+0]=e.n);a.p=c+b[c+6];a.g=c}if(!a.g)return a.z[0]}}var A=d.prototype;A[20]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e>>a[b+y(this)]}; +A[34]=function(){var a=this.z,b=a[this.g+7],c=y(this);a[b+c]=!a[b+y(this)]};A[3]=function(){var a=this.g,b=this.z,c=b[a+7],e=y(this);a=b[a+4].j[y(this)];b[c+e]=a.v?a.u:a.z[a.o]};A[24]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e<=a[b+y(this)]};A[22]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e>>13)>>>0}; +A[32]=function(){var a=this.z,b=a[this.g+7],c=y(this);a[b+c]=-a[b+y(this)]};A[43]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)],f=a[b+y(this)],h=y(this);if(h===65535)var k=a[b+y(this)];else{k=Array(h);for(var l=0;l>>a[b+y(this)]};A[37]=function(){var a=this.z,b=a[this.g+7],c=y(this);y(this);a[b+c]=void 0}; +A[41]=function(){var a=this.g,b=this.z,c=b[a+7],e=y(this),f=y(this);b[c+e]&&(b[a+0]=f)};A[11]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e+a[b+y(this)]};A[57]=function(){debugger};A[1]=function(){var a=this.z,b=a[this.g+7],c=y(this);a[b+c]=y(this)};A[46]=function(){var a=this.z;throw a[a[this.g+7]+y(this)];};A[14]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e/a[b+y(this)]}; +A[47]=function(){for(var a=this.g,b=this.z,c=b[a+7],e=y(this),f=y(this),h=y(this),k=y(this),l=y(this),p=y(this),n=Array(l),m=0;m=a[b+y(this)]};A[58]=function(){var a=this.g,b=this.z;b[a+0]=b[b[a+7]+y(this)]};A[51]=function(){var a=this.z,b=a[this.g+7],c=a[b+y(this)],e=a[b+y(this)];b=a[b+y(this)];a=Object.getOwnPropertyDescriptor(c,e);b={set:b,configurable:!0,enumerable:!0};a&&typeof a.get==="function"&&(b.get=a.get);Object.defineProperty(c,e,b)};A[17]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e|a[b+y(this)]}; +A[33]=function(){var a=this.z,b=a[this.g+7],c=y(this);a[b+c]=+a[b+y(this)]};A[38]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=x(this);e=Object.prototype.hasOwnProperty.call(this.h,e)?this.h[e]:void 0;a[b+c]=typeof e};A[23]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e>a[b+y(this)]};A[13]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e*a[b+y(this)]}; +A[59]=function(){var a=this.g,b=this.z,c=b[a+5];c||(b[a+5]=c=[]);c.push({n:y(this),Q:y(this),J:y(this),K:y(this)})};A[45]=function(){var a=this.g,b=this.z,c=b[b[a+7]+y(this)];this.f(a,"y","z",{},[]);var e=b[a+1],f=b[a+2];f&1&&(typeof c!=="object"||c===null)&&(c=b[a+3]);for(var h=a+b[a+6],k=a;k>1):0]=c};A[18]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e^a[b+y(this)]}; +A[7]=function(){var a=this.g,b=this.z,c=b[a+7],e=y(this);a=b[a+4].j[e];b=b[c+y(this)];a.e=69;a.v?a.u=b:a.z[a.o]=b;a.t=16};A[5]=function(){var a=this.z,b=a[this.g+7],c=y(this);a[b+c]=a[b+y(this)]};A[29]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e!=a[b+y(this)]}; +A[42]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)],f=y(this);if(f===65535)var h=a[b+y(this)];else{h=Array(f);for(var k=0;k=f.F.length?b[a+0]=h:b[c+e]=f.F[f.B++]};A[55]=function(){this.z[this.g+5].pop()};A[16]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e&a[b+y(this)]};A[12]=function(){var a=this.z,b=a[this.g+7],c=y(this),e=a[b+y(this)];a[b+c]=e-a[b+y(this)]};A[48]=function(){for(var a=this.z,b=a[this.g+7],c=y(this),e=y(this),f=Array(e),h=0;h>>13)&65535);return b};function fa(a,f){a.m?a.o=f:a.e[a.l]=f} +function ha(a,f,b){f=a.e[f+7]+b;b=a.b||(a.b=[]);return b[f]||(b[f]=new z(f,a.e))} +function R(a,f,b,l,H){var h;for(a.z(0,36,f,l,b);;){var g=a.r;b=a.e;var m=b[g+0];if(m>=a.g.length)break;b[g+0]=m+1;m=a.g[m];try{var d=b[g+7];switch(m){case 0:var e=Q(a);b[d+e]=a.a();break;case 1:e=Q(a,39);b[d+e]=Q(a,null);break;case 2:e=Q(a,"y");var I=a.a();if(!(I in a.E))throw new ReferenceError(`${I} is not defined`);b[d+e]=a.E[I];break;case 3:e=Q(a,"x");b[d+e]=b[g+4].h[Q(a,42)].n(null,46,88,void 0);break;case 4:e=Q(a,[]);b[d+e]=b[g+3];break;case 5:e=Q(a,42);b[d+e]=b[d+Q(a,"q")];break;case 6:a.E[a.a()]= +b[d+Q(a,[])];break;case 7:var ia=Q(a,"y");fa(b[g+4].h[ia],b[d+Q(a,[])]);break;case 8:e=Q(a,"z");var n=b[d+Q(a,{})],p=b[d+Q(a,"k")];b[d+e]=n[p];break;case 9:n=b[d+Q(a)];p=b[d+Q(a,{})];var w=b[d+Q(a)];Reflect.set(n,p,w);break;case 10:e=Q(a,[]);n=b[d+Q(a,[])];p=b[d+Q(a,"z")];b[d+e]=delete n[p];break;case 11:e=Q(a,null);var k=b[d+Q(a,"x")];b[d+e]=k+b[d+Q(a,null)];break;case 12:e=Q(a,[]);k=b[d+Q(a,"x")];b[d+e]=k-b[d+Q(a,[])];break;case 13:e=Q(a,9);k=b[d+Q(a,[])];b[d+e]=k*b[d+Q(a,{})];break;case 14:e=Q(a, +null);k=b[d+Q(a,{})];b[d+e]=k/b[d+Q(a,null)];break;case 15:e=Q(a,{});k=b[d+Q(a,"y")];b[d+e]=k%b[d+Q(a,[])];break;case 60:e=Q(a,69);k=b[d+Q(a,"y")];b[d+e]=Math.pow(k,b[d+Q(a,{})]);break;case 16:e=Q(a,{});k=b[d+Q(a,{})];b[d+e]=k&b[d+Q(a,"q")];break;case 17:e=Q(a,[]);k=b[d+Q(a,{})];b[d+e]=k|b[d+Q(a)];break;case 18:e=Q(a,51);k=b[d+Q(a,[])];b[d+e]=k^b[d+Q(a,null)];break;case 19:e=Q(a,null);k=b[d+Q(a,null)];b[d+e]=k<>b[d+Q(a,null)];break;case 21:e= +Q(a,{});k=b[d+Q(a)];b[d+e]=k>>>b[d+Q(a,[])];break;case 22:e=Q(a,88);k=b[d+Q(a,null)];b[d+e]=kb[d+Q(a,{})];break;case 24:e=Q(a,null);k=b[d+Q(a,[])];b[d+e]=k<=b[d+Q(a,{})];break;case 25:e=Q(a,99);k=b[d+Q(a,{})];b[d+e]=k>=b[d+Q(a,[])];break;case 26:e=Q(a,"k");k=b[d+Q(a,"k")];b[d+e]=k===b[d+Q(a,[])];break;case 27:e=Q(a,"k");k=b[d+Q(a,85)];b[d+e]=k!==b[d+Q(a,{})];break;case 28:e=Q(a,[]);k=b[d+Q(a,14)];b[d+e]=k==b[d+Q(a,"q")];break;case 29:e= +Q(a);k=b[d+Q(a,[])];b[d+e]=k!=b[d+Q(a)];break;case 30:e=Q(a,{});k=b[d+Q(a,{})];b[d+e]=k in b[d+Q(a,[])];break;case 31:e=Q(a,"q");n=b[d+Q(a,[])];b[d+e]=n instanceof b[d+Q(a)];break;case 32:e=Q(a);b[d+e]=-b[d+Q(a,"y")];break;case 33:e=Q(a,[]);b[d+e]=+b[d+Q(a,{})];break;case 34:e=Q(a,{});b[d+e]=!b[d+Q(a,null)];break;case 35:e=Q(a,"y");b[d+e]=~b[d+Q(a,[])];break;case 36:e=Q(a,"k");b[d+e]=typeof b[d+Q(a,null)];break;case 37:e=Q(a,null);Q(a,{});b[d+e]=void 0;break;case 38:e=Q(a,{});var T=a.a();w=Object.prototype.hasOwnProperty.call(a.E, +T)?a.E[T]:void 0;b[d+e]=typeof w;break;case 39:b[g+0]=Q(a);break;case 40:var J=Q(a,null),K=Q(a);b[d+J]||(b[g+0]=K);break;case 41:J=Q(a,[]);K=Q(a,[]);b[d+J]&&(b[g+0]=K);break;case 42:e=Q(a,"z");var q=b[d+Q(a,62)],r=Q(a,24);if(r===65535)l=b[d+Q(a,"y")];else for(l=Array(r),h=0;h>1):0]=C;break;case 46:throw b[d+ +Q(a,null)];case 47:e=Q(a,"y");var ka=Q(a,"x"),la=Q(a,[]),ma=Q(a,0),W=Q(a),na=Q(a,"x"),M=Array(W);for(h=0;h=F.p.length?b[g+0]=ta:b[d+e]=F.p[F.B++]; +break;case 54:var u=b[g+5];u||(b[g+5]=u=[]);u.push({J:Q(a),u:Q(a,{})});break;case 55:b[g+5].pop();break;case 59:(u=b[g+5])||(b[g+5]=u=[]);u.push({I:Q(a,null),C:Q(a,[]),D:Q(a,null),G:Q(a,{})});break;case 56:var ca=Q(a,null),da=Q(a,{}),ua=Q(a),G=Q(a)^ca|0;for(g=da;g>>13)>>>0;break;case 58:b[g+0]=b[d+Q(a,"q")];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+" at pc "+(b[g+0]-1));}}catch(x){g=0;for(m=a.r;m;){var v=b[m+5];if(v&& +v.length>0){g=m;break}P(a,m);a.i=m;m=b[m+1];a.r=m}if(!g)throw x;m=b[g+5].pop();v=b[g+7];m.u!==void 0?(b[v+m.u]=x,b[g+0]=m.J):(b[v+m.C]=m.G,b[v+m.D]=x,b[g+0]=m.I);a.i=g+b[g+6];a.r=g}if(!a.r)return a.e[0]}a.q=H}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new B(S,["window","TEST_OUTPUT",4,void 0,0,1],[4,1,47,3,39,1,7,0,0,5,2,3,2,3,0,0,0,4,1,0,0,5,2,0,42,6,2,1,5,9,3,4,6,0,3,3,0,45,3,4,2,0,5,4,0,5,3,5,0,5,4,0,5,4,5,22,5,4,0,40,5,85,11,5,3,4,5,3,5,5,5,4,0,5,5,0,11,6,4,5,5,4,6,39,55,45,3,0,5,3,0,45,5]),new A(void 0,{k:0,j:7,x:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-layers/generation.json b/test/vm/jsconfuser-vm/fixtures/runtime-layers/generation.json new file mode 100644 index 00000000..3c622068 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-layers/generation.json @@ -0,0 +1,96 @@ +{ + "note": "Each encoded file is the VM encoder's output for source.js with the listed options, the leading disassembly comment block removed (the committed corpus format). Math.random was replaced by the seeded PRNG used by cff-loop-return (mulberry32: state += 0x6d2b79f5, then the standard mulberry32 output). minify runs the Closure Compiler and needs a JDK on PATH. Regenerating with the same encoder revision, options and seed reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5" + }, + "files": { + "class-minify.encoded.js": { + "options": { + "classObfuscation": true, + "minify": true + }, + "seed": 324508639 + }, + "class-minify-table-shuffle.encoded.js": { + "options": { + "classObfuscation": true, + "minify": true, + "handlerTable": true, + "shuffleOpcodes": true + }, + "seed": 324508639 + }, + "class-minify-aliased.encoded.js": { + "options": { + "classObfuscation": true, + "minify": true, + "aliasedOpcodes": true + }, + "seed": 1 + }, + "class-minify-specialized.encoded.js": { + "options": { + "classObfuscation": true, + "minify": true, + "specializedOpcodes": true + }, + "seed": 2 + }, + "minify-aliased.encoded.js": { + "options": { + "minify": true, + "aliasedOpcodes": true + }, + "seed": 324508639 + }, + "minify-randomized.encoded.js": { + "options": { + "minify": true, + "randomizeOpcodes": true + }, + "seed": 324508639 + }, + "minify-table-randomized.encoded.js": { + "options": { + "minify": true, + "handlerTable": true, + "randomizeOpcodes": true + }, + "seed": 324508639 + }, + "minify-table-aliased.encoded.js": { + "options": { + "minify": true, + "handlerTable": true, + "aliasedOpcodes": true + }, + "seed": 324508639 + }, + "minify-table-macro.encoded.js": { + "options": { + "minify": true, + "handlerTable": true, + "macroOpcodes": true + }, + "seed": 324508639 + }, + "minify-table-specialized.encoded.js": { + "options": { + "minify": true, + "handlerTable": true, + "specializedOpcodes": true + }, + "seed": 324508639 + }, + "minify-table-anti.encoded.js": { + "options": { + "minify": true, + "handlerTable": true, + "antiInstrumentation": true + }, + "seed": 324508639 + } + }, + "claim": "runtime-layer options re-spell the interpreter (class obfuscation, Closure ADVANCED minify, handler table, case shuffle) and leave the payload unchanged: extract-container locates words, pool and root descriptor by the boot skeleton, the added-opcode readers find handlers by behaviour in a minified runtime, and randomized opcode values get their meaning from handler behaviour, so the decode behaves as the source" +} diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-aliased.encoded.js b/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-aliased.encoded.js new file mode 100644 index 00000000..1321ff92 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-aliased.encoded.js @@ -0,0 +1,17 @@ +var c={};function da(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ea(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function fa(a){return a.C?a.F:a.g[a.B]}function va(a,g){a.C?a.F=g:a.g[a.B]=g}function wa(a){this.U=a;this.o=[];this.prototype={}}function xa(a,g,b){this.l=a;this.K=g;this.i=b;this.D=null;this.g=[];this.j=1;this.h=0}function C(a){return a.l[a.g[a.h+0]++]} +function I(a,g,b,m,k){for(var f=g.U,l=a.g,n=a.j,B=8+f.v,d=n+B;l.length>>13)&65535);return g}function K(a,g){var b=a.D;if(b){var m=a.g;a=g+m[g+6];for(g=m[g+7];g=l.length)break;b[f+0]=n+1;var B=a.l[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%l.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=B);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(B){case 0:var e=C(a);b[d+e]=J(a);break;case 1:e=C(a);b[d+e]=C(a);break;case 2:e=C(a);var w=J(a);if(!(w in a.i))throw new ReferenceError(`${w} is not defined`);b[d+e]=a.i[w];break; +case 3:e=C(a);b[d+e]=fa(b[f+4].o[C(a)]);break;case 4:e=C(a);b[d+e]=b[f+3];break;case 5:e=C(a);b[d+e]=b[d+C(a)];break;case 6:a.i[J(a)]=b[d+C(a)];break;case 7:var Aa=C(a);va(b[f+4].o[Aa],b[d+C(a)]);break;case 8:e=C(a);var q=b[d+C(a)],r=b[d+C(a)];b[d+e]=q[r];break;case 9:q=b[d+C(a)];r=b[d+C(a)];var x=b[d+C(a)];Reflect.set(q,r,x);break;case 10:e=C(a);q=b[d+C(a)];r=b[d+C(a)];b[d+e]=delete q[r];break;case 11:e=C(a);var h=b[d+C(a)];b[d+e]=h+b[d+C(a)];break;case 12:e=C(a);h=b[d+C(a)];b[d+e]=h-b[d+C(a)];break; +case 13:e=C(a);h=b[d+C(a)];b[d+e]=h*b[d+C(a)];break;case 14:e=C(a);h=b[d+C(a)];b[d+e]=h/b[d+C(a)];break;case 15:e=C(a);h=b[d+C(a)];b[d+e]=h%b[d+C(a)];break;case 60:e=C(a);h=b[d+C(a)];b[d+e]=Math.pow(h,b[d+C(a)]);break;case 16:e=C(a);h=b[d+C(a)];b[d+e]=h&b[d+C(a)];break;case 17:e=C(a);h=b[d+C(a)];b[d+e]=h|b[d+C(a)];break;case 18:e=C(a);h=b[d+C(a)];b[d+e]=h^b[d+C(a)];break;case 19:e=C(a);h=b[d+C(a)];b[d+e]=h<>b[d+C(a)];break;case 21:e=C(a);h=b[d+ +C(a)];b[d+e]=h>>>b[d+C(a)];break;case 22:e=C(a);h=b[d+C(a)];b[d+e]=hb[d+C(a)];break;case 24:e=C(a);h=b[d+C(a)];b[d+e]=h<=b[d+C(a)];break;case 25:e=C(a);h=b[d+C(a)];b[d+e]=h>=b[d+C(a)];break;case 26:e=C(a);h=b[d+C(a)];b[d+e]=h===b[d+C(a)];break;case 27:e=C(a);h=b[d+C(a)];b[d+e]=h!==b[d+C(a)];break;case 28:e=C(a);h=b[d+C(a)];b[d+e]=h==b[d+C(a)];break;case 29:e=C(a);h=b[d+C(a)];b[d+e]=h!=b[d+C(a)];break;case 30:e=C(a);h=b[d+C(a)];b[d+e]=h in b[d+C(a)]; +break;case 31:e=C(a);q=b[d+C(a)];b[d+e]=q instanceof b[d+C(a)];break;case 32:e=C(a);b[d+e]=-b[d+C(a)];break;case 33:e=C(a);b[d+e]=+b[d+C(a)];break;case 34:e=C(a);b[d+e]=!b[d+C(a)];break;case 35:e=C(a);b[d+e]=~b[d+C(a)];break;case 36:e=C(a);b[d+e]=typeof b[d+C(a)];break;case 37:e=C(a);C(a);b[d+e]=void 0;break;case 38:e=C(a);var ha=J(a);x=Object.prototype.hasOwnProperty.call(a.i,ha)?a.i[ha]:void 0;b[d+e]=typeof x;break;case 39:b[f+0]=C(a);break;case 40:var E=C(a),F=C(a);b[d+E]||(b[f+0]=F);break;case 41:E= +C(a);F=C(a);b[d+E]&&(b[f+0]=F);break;case 42:e=C(a);var t=b[d+C(a)],u=C(a);if(u===65535)m=b[d+C(a)];else for(m=Array(u),k=0;k>1):0]=v;break;case 46:throw b[d+C(a)];case 47:e=C(a);var Ba=C(a),Ca=C(a),Da=C(a),ja=C(a),Ea=C(a),R=Array(ja);for(k=0;k=N.J.length?b[f+0]=La:b[d+e]=N.J[N.M++];break;case 54:var D=b[f+5];D||(b[f+5]=D=[]);D.push({V:C(a),L:C(a)});break;case 55:b[f+5].pop();break;case 59:(D=b[f+5])||(b[f+5]=D=[]);D.push({T:C(a),S:C(a),Y:C(a),Z:C(a)});break;case 56:var pa=C(a),qa=C(a),Ma=C(a),O=C(a)^pa|0;for(f=qa;f>>13)>>>0;break;case 58:b[f+0]=b[d+C(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+B+" at pc "+(b[f+0]-1));case 61:let A=[C(a),C(a),C(a)],U=[A[2],A[1],A[0]];e=U[0];b[d+e]=J(a,U[1],U[2]);break;case 62:let ra=[C(a),C(a)],sa=[ra[1],ra[0]];e=sa[0];b[d+e]=b[d+sa[1]];break;case 63:let Na=[C(a)];v=b[d+Na[0]];K(a,f);y=b[f+1];G=b[f+2];G&1&&(typeof v!=="object"||v===null)&&(v=b[f+3]);Q=f+b[f+6];for(k=f;k>1):0]=v;break; +case 64:e=C(a);b[d+e]=b[f+3];break;case 65:let V=[C(a),C(a),C(a)],W=[V[2],V[0],V[1]];e=W[0];h=b[d+W[1]];b[d+e]=h+b[d+W[2]];break;case 66:let X=[C(a),C(a),C(a)],Y=[X[0],X[2],X[1]];e=Y[0];w=J(a,Y[1],Y[2]);if(!(w in a.i))throw new ReferenceError(`${w} is not defined`);b[d+e]=a.i[w];break;case 67:let Z=[C(a),C(a),C(a)],aa=[Z[1],Z[0],Z[2]];q=b[d+aa[0]];r=b[d+aa[1]];x=b[d+aa[2]];Reflect.set(q,r,x);break;case 68:let ba=[C(a),C(a),C(a)],ca=[ba[2],ba[1],ba[0]];e=ca[0];h=b[d+ca[1]];b[d+e]=h0){f=l;break}K(a,l);a.j=l;l=b[l+1];a.h=l}if(!f)throw A;l=b[f+5].pop();n=b[f+7];l.L!==void 0?(b[n+l.L]=A,b[f+0]=l.V):(b[n+l.S]=l.Z,b[n+l.Y]=A,b[f+0]=l.T);a.j=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var P=globalThis;typeof window!=="undefined"&&(P.window=window,P.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(P.module=module,P.exports=typeof exports!=="undefined"?exports:void 0);za(new xa([64,1,47,3,39,1,7,0,0,62,3,2,66,3,0,0,61,0,1,4,61,0,2,5,42,6,2,1,5,67,4,3,6,61,0,3,3,63,3,64,2,61,0,4,5,62,5,3,61,0,4,5,62,5,4,68,0,4,5,69,85,5,65,3,4,5,62,5,3,62,4,5,61,0,5,5,65,4,5,6,62,6,4,70,55,63,3,61,0,3,5,63,5],["window","TEST_OUTPUT",4,void 0,0,1],P),new wa({m:0,v:7,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-randomized.encoded.js b/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-randomized.encoded.js new file mode 100644 index 00000000..dee5021c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-randomized.encoded.js @@ -0,0 +1,16 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var q=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.S=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=16;this.h=0}function D(a){return a.j[a.g[a.h+2]++]} +function E(a,g,b,l,m){for(var f=g.S,k=a.g,n=a.l,w=13+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+1];for(g=l[g+9];g=k.length)break;b[f+2]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.O==="number"&&(b[f+c.O]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+9];switch(w){case 58675:var e=D(a);b[d+e]=R(a);break;case 59513:e=D(a);b[d+e]=D(a);break;case 9745:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K]; +break;case 62699:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 63815:e=D(a);b[d+e]=b[f+3];break;case 51108:e=D(a);b[d+e]=b[d+D(a)];break;case 58865:a.i[R(a)]=b[d+D(a)];break;case 5169:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 9119:e=D(a);var p=b[d+D(a)],r=b[d+D(a)];b[d+e]=p[r];break;case 21956:p=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(p,r,y);break;case 37517:e=D(a);p=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete p[r];break;case 45047:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 58367:e= +D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break;case 3343:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 15331:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 37910:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 54744:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 61072:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 22373:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 49895:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 24807:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 1383:e=D(a);h=b[d+D(a)];b[d+e]=h>>>b[d+D(a)];break;case 6858:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 49605:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 62024:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 37588:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 54099:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 9938:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 31693:e= +D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 58249:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)];break;case 55198:e=D(a);p=b[d+D(a)];b[d+e]=p instanceof b[d+D(a)];break;case 24245:e=D(a);b[d+e]=-b[d+D(a)];break;case 30173:e=D(a);b[d+e]=+b[d+D(a)];break;case 24355:e=D(a);b[d+e]=!b[d+D(a)];break;case 2075:e=D(a);b[d+e]=~b[d+D(a)];break;case 42683:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 2210:e=D(a);D(a);b[d+e]=void 0;break;case 31433:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]: +void 0;b[d+e]=typeof y;break;case 1248:b[f+2]=D(a);break;case 62628:var L=D(a),M=D(a);b[d+L]||(b[f+2]=M);break;case 51402:L=D(a);M=D(a);b[d+L]&&(b[f+2]=M);break;case 31339:e=D(a);var t=b[d+D(a)],u=D(a);if(u===1638052255)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 31958:throw b[d+D(a)];case 3126:e=D(a);var oa=D(a),pa=D(a),qa= +D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+2]=xa:b[d+e]=I.J[I.M++];break;case 2373:var x=b[f+11];x||(b[f+11]=x=[]);x.push({T:D(a),L:D(a)});break;case 56388:b[f+ +11].pop();break;case 4416:(x=b[f+11])||(b[f+11]=x=[]);x.push({R:D(a),P:D(a),W:D(a),X:D(a)});break;case 43666:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>>13)>>>0;break;case 30352:b[f+2]=b[d+D(a)];break;case 30171:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+2]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+11])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+8];a.h=k}if(!f)throw z;k=b[f+11].pop();n=b[f+9];k.L!==void 0?(b[n+ +k.L]=z,b[f+2]=k.T):(b[n+k.P]=k.X,b[n+k.W]=z,b[f+2]=k.R);a.l=f+b[f+1];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new C([63815,1,3126,3,39,1,7,0,0,51108,2,3,9745,3,0,0,58675,4,1,0,58675,5,2,0,31339,6,2,1,5,21956,3,4,6,58675,3,3,0,10097,3,63815,2,58675,5,4,0,51108,3,5,58675,5,4,0,51108,4,5,6858,5,4,0,62628,5,85,45047,5,3,4,51108,3,5,51108,5,4,58675,5,5,0,45047,6,4,5,51108,4,6,1248,55,10097,3,58675,5,3,0,10097,5],["window","TEST_OUTPUT",4,void 0,0,1],U),new B({m:0,v:7,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-aliased.encoded.js b/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-aliased.encoded.js new file mode 100644 index 00000000..c7fdf992 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-aliased.encoded.js @@ -0,0 +1,25 @@ +var d={};function g(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}var m=new WeakMap;function r(a,b){this.g=a;this.B=b;this.C=!1;this.F=void 0}function t(a){this.U=a;this.o=[];this.prototype={}}function u(a,b,c){this.l=a;this.K=b;this.i=c;this.D=null;this.g=[];this.j=1;this.h=0}function v(a){return a.l[a.g[a.h+0]++]} +function x(a,b,c,e,f){for(var h=b.U,k=a.g,l=a.j,q=8+h.v,p=l+q;k.length>>13)&65535);return b}function A(a,b){var c=a.D;if(c){var e=a.g;a=b+e[b+6];for(b=e[b+7];b=e.length)break;b[c+0]=f+1;var h=a.l[f];typeof d.u==="number"&&(b[c+d.u]=(b[c+d.u]+1)%e.length);typeof d.A==="number"&&b[c+d.A]++;typeof d.P==="number"&&(b[c+d.P]=h);typeof d.H==="number"&&(b[c+d.H]=b[c+d.H]^f|0);try{a[h]()}catch(k){c=0;for(e=a.h;e;){if((f=b[e+5])&&f.length>0){c=e;break}A(a,e);a.j=e;e=b[e+1];a.h=e}if(!c)throw k;e=b[c+5].pop();f=b[c+7];e.L!==void 0?(b[f+e.L]=k,b[c+0]=e.V):(b[f+e.S]=e.Z,b[f+e.Y]=k,b[c+0]=e.T); +a.j=c+b[c+6];a.h=c}if(!a.h)return a.g[0]}}var C=u.prototype;C[0]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=z(this)};C[1]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=v(this)};C[2]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=z(this);if(!(e in this.i))throw new ReferenceError(`${e} is not defined`);a[b+c]=this.i[e]};C[3]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);a=b[a+4].o[v(this)];b[c+e]=a.C?a.F:a.g[a.B]}; +C[4]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);b[c+e]=b[a+3]};C[5]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=a[b+v(this)]};C[6]=function(){var a=this.g,b=a[this.h+7];this.i[z(this)]=a[b+v(this)]};C[7]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);a=b[a+4].o[e];b=b[c+v(this)];a.C?a.F=b:a.g[a.B]=b};C[8]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)],f=a[b+v(this)];a[b+c]=e[f]}; +C[9]=function(){var a=this.g,b=a[this.h+7],c=a[b+v(this)],e=a[b+v(this)];a=a[b+v(this)];Reflect.set(c,e,a)};C[10]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)],f=a[b+v(this)];a[b+c]=delete e[f]};C[11]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e+a[b+v(this)]};C[12]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e-a[b+v(this)]};C[13]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e*a[b+v(this)]}; +C[14]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e/a[b+v(this)]};C[15]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e%a[b+v(this)]};C[60]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=Math.pow(e,a[b+v(this)])};C[16]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e&a[b+v(this)]};C[17]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e|a[b+v(this)]}; +C[18]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e^a[b+v(this)]};C[19]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e<>a[b+v(this)]};C[21]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e>>>a[b+v(this)]};C[22]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=ea[b+v(this)]};C[24]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e<=a[b+v(this)]};C[25]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e>=a[b+v(this)]};C[26]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e===a[b+v(this)]};C[27]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e!==a[b+v(this)]}; +C[28]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e==a[b+v(this)]};C[29]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e!=a[b+v(this)]};C[30]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e in a[b+v(this)]};C[31]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e instanceof a[b+v(this)]};C[32]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=-a[b+v(this)]}; +C[33]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=+a[b+v(this)]};C[34]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=!a[b+v(this)]};C[35]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=~a[b+v(this)]};C[36]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=typeof a[b+v(this)]};C[37]=function(){var a=this.g,b=a[this.h+7],c=v(this);v(this);a[b+c]=void 0}; +C[38]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=z(this);e=Object.prototype.hasOwnProperty.call(this.i,e)?this.i[e]:void 0;a[b+c]=typeof e};C[39]=function(){this.g[this.h+0]=v(this)};C[40]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this),f=v(this);b[c+e]||(b[a+0]=f)};C[41]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this),f=v(this);b[c+e]&&(b[a+0]=f)}; +C[42]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)],f=v(this);if(f===65535)var h=a[b+v(this)];else{h=Array(f);for(var k=0;k>1):0]=c}; +C[46]=function(){var a=this.g;throw a[a[this.h+7]+v(this)];}; +C[47]=function(){for(var a=this.h,b=this.g,c=b[a+7],e=v(this),f=v(this),h=v(this),k=v(this),l=v(this),q=v(this),p=Array(l),n=0;n=f.J.length?b[a+0]=h:b[c+e]=f.J[f.M++]}; +C[54]=function(){var a=this.h,b=this.g,c=b[a+5];c||(b[a+5]=c=[]);c.push({V:v(this),L:v(this)})};C[55]=function(){this.g[this.h+5].pop()};C[59]=function(){var a=this.h,b=this.g,c=b[a+5];c||(b[a+5]=c=[]);c.push({T:v(this),S:v(this),Y:v(this),Z:v(this)})};C[56]=function(){for(var a=v(this),b=v(this),c=v(this),e=v(this)^a|0,f=b;f>>13)>>>0};C[58]=function(){var a=this.h,b=this.g;b[a+0]=b[b[a+7]+v(this)]};C[57]=function(){debugger}; +C[61]=function(){var a=this.g,b=a[this.h+7],c=[v(this),v(this),v(this)];c=[c[2],c[1],c[0]];a[b+c[0]]=z(this,c[1],c[2])};C[62]=function(){var a=this.g,b=a[this.h+7],c=[v(this),v(this)];c=[c[1],c[0]];a[b+c[0]]=a[b+c[1]]};C[63]=function(){var a=this.h,b=this.g,c=b[a+7],e=[v(this)];c=b[c+e[0]];A(this,a);e=b[a+1];var f=b[a+2];f&1&&(typeof c!=="object"||c===null)&&(c=b[a+3]);for(var h=a+b[a+6],k=a;k>1):0]=c}; +C[64]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);b[c+e]=b[a+3]};C[65]=function(){var a=this.g,b=a[this.h+7],c=[v(this),v(this),v(this)];c=[c[2],c[0],c[1]];a[b+c[0]]=a[b+c[1]]+a[b+c[2]]};C[66]=function(){var a=this.g,b=a[this.h+7],c=[v(this),v(this),v(this)],e=[c[0],c[2],c[1]];c=e[0];e=z(this,e[1],e[2]);if(!(e in this.i))throw new ReferenceError(`${e} is not defined`);a[b+c]=this.i[e]}; +C[67]=function(){var a=this.g,b=a[this.h+7],c=[v(this),v(this),v(this)];c=[c[1],c[0],c[2]];Reflect.set(a[b+c[0]],a[b+c[1]],a[b+c[2]])};C[68]=function(){var a=this.g,b=a[this.h+7],c=[v(this),v(this),v(this)];c=[c[2],c[1],c[0]];a[b+c[0]]=a[b+c[1]]>>13)&65535);return b}function A(a,b){var c=a.D;if(c){var d=a.g;a=b+d[b+6];for(b=d[b+7];b=d.length)break;b[c+0]=f+1;var h=a.j[f];typeof e.u==="number"&&(b[c+e.u]=(b[c+e.u]+1)%d.length);typeof e.A==="number"&&b[c+e.A]++;typeof e.P==="number"&&(b[c+e.P]=h);typeof e.H==="number"&&(b[c+e.H]=b[c+e.H]^f|0);try{a[h]()}catch(k){c=0;for(d=a.h;d;){if((f=b[d+5])&&f.length>0){c=d;break}A(a,d);a.l=d;d=b[d+1];a.h=d}if(!c)throw k;d=b[c+5].pop();f=b[c+7];d.L!==void 0?(b[f+d.L]=k,b[c+0]=d.V):(b[f+d.S]=d.Z,b[f+d.Y]=k,b[c+0]=d.T); +a.l=c+b[c+6];a.h=c}if(!a.h)return a.g[0]}}var C=u.prototype;C[0]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=z(this)};C[1]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=v(this)};C[2]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=z(this);if(!(d in this.i))throw new ReferenceError(`${d} is not defined`);a[b+c]=this.i[d]};C[3]=function(){var a=this.h,b=this.g,c=b[a+7],d=v(this);a=b[a+4].o[v(this)];b[c+d]=a.C?a.F:a.g[a.B]}; +C[4]=function(){var a=this.h,b=this.g,c=b[a+7],d=v(this);b[c+d]=b[a+3]};C[5]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=a[b+v(this)]};C[6]=function(){var a=this.g,b=a[this.h+7];this.i[z(this)]=a[b+v(this)]};C[7]=function(){var a=this.h,b=this.g,c=b[a+7],d=v(this);a=b[a+4].o[d];b=b[c+v(this)];a.C?a.F=b:a.g[a.B]=b};C[8]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)],f=a[b+v(this)];a[b+c]=d[f]}; +C[9]=function(){var a=this.g,b=a[this.h+7],c=a[b+v(this)],d=a[b+v(this)];a=a[b+v(this)];Reflect.set(c,d,a)};C[10]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)],f=a[b+v(this)];a[b+c]=delete d[f]};C[11]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d+a[b+v(this)]};C[12]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d-a[b+v(this)]};C[13]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d*a[b+v(this)]}; +C[14]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d/a[b+v(this)]};C[15]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d%a[b+v(this)]};C[60]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=Math.pow(d,a[b+v(this)])};C[16]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d&a[b+v(this)]};C[17]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d|a[b+v(this)]}; +C[18]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d^a[b+v(this)]};C[19]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d<>a[b+v(this)]};C[21]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d>>>a[b+v(this)]};C[22]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=da[b+v(this)]};C[24]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d<=a[b+v(this)]};C[25]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d>=a[b+v(this)]};C[26]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d===a[b+v(this)]};C[27]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d!==a[b+v(this)]}; +C[28]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d==a[b+v(this)]};C[29]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d!=a[b+v(this)]};C[30]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d in a[b+v(this)]};C[31]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)];a[b+c]=d instanceof a[b+v(this)]};C[32]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=-a[b+v(this)]}; +C[33]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=+a[b+v(this)]};C[34]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=!a[b+v(this)]};C[35]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=~a[b+v(this)]};C[36]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=typeof a[b+v(this)]};C[37]=function(){var a=this.g,b=a[this.h+7],c=v(this);v(this);a[b+c]=void 0}; +C[38]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=z(this);d=Object.prototype.hasOwnProperty.call(this.i,d)?this.i[d]:void 0;a[b+c]=typeof d};C[39]=function(){this.g[this.h+0]=v(this)};C[40]=function(){var a=this.h,b=this.g,c=b[a+7],d=v(this),f=v(this);b[c+d]||(b[a+0]=f)};C[41]=function(){var a=this.h,b=this.g,c=b[a+7],d=v(this),f=v(this);b[c+d]&&(b[a+0]=f)}; +C[42]=function(){var a=this.g,b=a[this.h+7],c=v(this),d=a[b+v(this)],f=v(this);if(f===65535)var h=a[b+v(this)];else{h=Array(f);for(var k=0;k>1):0]=c}; +C[46]=function(){var a=this.g;throw a[a[this.h+7]+v(this)];}; +C[47]=function(){for(var a=this.h,b=this.g,c=b[a+7],d=v(this),f=v(this),h=v(this),k=v(this),l=v(this),q=v(this),p=Array(l),n=0;n=f.J.length?b[a+0]=h:b[c+d]=f.J[f.M++]}; +C[54]=function(){var a=this.h,b=this.g,c=b[a+5];c||(b[a+5]=c=[]);c.push({V:v(this),L:v(this)})};C[55]=function(){this.g[this.h+5].pop()};C[59]=function(){var a=this.h,b=this.g,c=b[a+5];c||(b[a+5]=c=[]);c.push({T:v(this),S:v(this),Y:v(this),Z:v(this)})};C[56]=function(){for(var a=v(this),b=v(this),c=v(this),d=v(this)^a|0,f=b;f>>13)>>>0};C[58]=function(){var a=this.h,b=this.g;b[a+0]=b[b[a+7]+v(this)]};C[57]=function(){debugger}; +C[61]=function(){var a=this.h,b=this.g,c=b[a+7],d=[v(this),v(this),v(this),v(this),v(this),v(this),v(this),v(this),v(this),v(this),v(this),v(this),v(this),v(this)];d=[d[9],d[4],d[8],d[3],d[13],d[6],d[5],d[12],d[11],d[10],d[0],d[1],d[2],d[7]];var f=d[1];b[c+d[0]]||(b[a+0]=f);a=d[2];b[c+a]=Math.pow(b[c+d[3]],b[c+d[4]]);a=d[5];b[c+a]=b[c+d[6]];a=d[7];var h=d[8],k={};for(f=0;f>>13)&65535);return b}function A(a,b){var c=a.D;if(c){var e=a.g;a=b+e[b+6];for(b=e[b+7];b=e.length)break;b[c+0]=f+1;var h=a.l[f];typeof d.u==="number"&&(b[c+d.u]=(b[c+d.u]+1)%e.length);typeof d.A==="number"&&b[c+d.A]++;typeof d.P==="number"&&(b[c+d.P]=h);typeof d.H==="number"&&(b[c+d.H]=b[c+d.H]^f|0);try{a[h]()}catch(k){c=0;for(e=a.h;e;){if((f=b[e+5])&&f.length>0){c=e;break}A(a,e);a.j=e;e=b[e+1];a.h=e}if(!c)throw k;e=b[c+5].pop();f=b[c+7];e.L!==void 0?(b[f+e.L]=k,b[c+0]=e.V):(b[f+e.S]=e.Z,b[f+e.Y]=k,b[c+0]=e.T); +a.j=c+b[c+6];a.h=c}if(!a.h)return a.g[0]}}var C=u.prototype;C[0]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=z(this)};C[1]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=v(this)};C[2]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=z(this);if(!(e in this.i))throw new ReferenceError(`${e} is not defined`);a[b+c]=this.i[e]};C[3]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);a=b[a+4].o[v(this)];b[c+e]=a.C?a.F:a.g[a.B]}; +C[4]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);b[c+e]=b[a+3]};C[5]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=a[b+v(this)]};C[6]=function(){var a=this.g,b=a[this.h+7];this.i[z(this)]=a[b+v(this)]};C[7]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);a=b[a+4].o[e];b=b[c+v(this)];a.C?a.F=b:a.g[a.B]=b};C[8]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)],f=a[b+v(this)];a[b+c]=e[f]}; +C[9]=function(){var a=this.g,b=a[this.h+7],c=a[b+v(this)],e=a[b+v(this)];a=a[b+v(this)];Reflect.set(c,e,a)};C[10]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)],f=a[b+v(this)];a[b+c]=delete e[f]};C[11]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e+a[b+v(this)]};C[12]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e-a[b+v(this)]};C[13]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e*a[b+v(this)]}; +C[14]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e/a[b+v(this)]};C[15]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e%a[b+v(this)]};C[60]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=Math.pow(e,a[b+v(this)])};C[16]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e&a[b+v(this)]};C[17]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e|a[b+v(this)]}; +C[18]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e^a[b+v(this)]};C[19]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e<>a[b+v(this)]};C[21]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e>>>a[b+v(this)]};C[22]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=ea[b+v(this)]};C[24]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e<=a[b+v(this)]};C[25]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e>=a[b+v(this)]};C[26]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e===a[b+v(this)]};C[27]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e!==a[b+v(this)]}; +C[28]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e==a[b+v(this)]};C[29]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e!=a[b+v(this)]};C[30]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e in a[b+v(this)]};C[31]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e instanceof a[b+v(this)]};C[32]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=-a[b+v(this)]}; +C[33]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=+a[b+v(this)]};C[34]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=!a[b+v(this)]};C[35]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=~a[b+v(this)]};C[36]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=typeof a[b+v(this)]};C[37]=function(){var a=this.g,b=a[this.h+7],c=v(this);v(this);a[b+c]=void 0}; +C[38]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=z(this);e=Object.prototype.hasOwnProperty.call(this.i,e)?this.i[e]:void 0;a[b+c]=typeof e};C[39]=function(){this.g[this.h+0]=v(this)};C[40]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this),f=v(this);b[c+e]||(b[a+0]=f)};C[41]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this),f=v(this);b[c+e]&&(b[a+0]=f)}; +C[42]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)],f=v(this);if(f===65535)var h=a[b+v(this)];else{h=Array(f);for(var k=0;k>1):0]=c}; +C[46]=function(){var a=this.g;throw a[a[this.h+7]+v(this)];}; +C[47]=function(){for(var a=this.h,b=this.g,c=b[a+7],e=v(this),f=v(this),h=v(this),k=v(this),l=v(this),q=v(this),p=Array(l),n=0;n=f.J.length?b[a+0]=h:b[c+e]=f.J[f.M++]}; +C[54]=function(){var a=this.h,b=this.g,c=b[a+5];c||(b[a+5]=c=[]);c.push({V:v(this),L:v(this)})};C[55]=function(){this.g[this.h+5].pop()};C[59]=function(){var a=this.h,b=this.g,c=b[a+5];c||(b[a+5]=c=[]);c.push({T:v(this),S:v(this),Y:v(this),Z:v(this)})};C[56]=function(){for(var a=v(this),b=v(this),c=v(this),e=v(this)^a|0,f=b;f>>13)>>>0};C[58]=function(){var a=this.h,b=this.g;b[a+0]=b[b[a+7]+v(this)]};C[57]=function(){debugger}; +C[61]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);b[c+e]=z(this);c=b[c+v(this)];A(this,a);e=b[a+1];var f=b[a+2];f&1&&(typeof c!=="object"||c===null)&&(c=b[a+3]);for(var h=a+b[a+6],k=a;k>1):0]=c};C[62]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=z(this);c=v(this);a[b+c]=a[b+v(this)]};C[63]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=a[b+v(this)];c=v(this);a[b+c]=z(this)}; +C[64]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e+a[b+v(this)];c=v(this);a[b+c]=a[b+v(this)]};var D=globalThis;typeof window!=="undefined"&&(D.window=window,D.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(D.module=module,D.exports=typeof exports!=="undefined"?exports:void 0); +B(new u([4,1,47,3,38,1,7,0,0,5,2,3,2,3,0,0,0,4,1,0,0,5,2,0,42,6,2,1,5,9,3,4,6,61,3,3,0,3,4,2,62,5,4,0,3,5,62,5,4,0,4,5,22,5,4,0,40,5,79,64,5,3,4,3,5,63,5,4,6,5,0,64,5,4,6,4,5,39,52,45,3,61,5,3,0,5],["window","TEST_OUTPUT",4,void 0,0,1],D),new t({m:0,v:7,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-randomized.encoded.js b/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-randomized.encoded.js new file mode 100644 index 00000000..0f3ed736 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-randomized.encoded.js @@ -0,0 +1,23 @@ +var d={};function g(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}var n=new WeakMap;function r(a,b){this.g=a;this.B=b;this.C=!1;this.F=void 0}function t(a){this.S=a;this.o=[];this.prototype={}}function u(a,b,c){this.j=a;this.K=b;this.i=c;this.D=null;this.g=[];this.l=16;this.h=0}function v(a){return a.j[a.g[a.h+2]++]} +function x(a,b,c,e,f){for(var h=b.S,k=a.g,l=a.l,q=13+h.v,p=l+q;k.length>>13)&65535);return b}function A(a,b){var c=a.D;if(c){var e=a.g;a=b+e[b+1];for(b=e[b+9];b=e.length)break;b[c+2]=f+1;var h=a.j[f];typeof d.u==="number"&&(b[c+d.u]=(b[c+d.u]+1)%e.length);typeof d.A==="number"&&b[c+d.A]++;typeof d.O==="number"&&(b[c+d.O]=h);typeof d.H==="number"&&(b[c+d.H]=b[c+d.H]^f|0);try{a[h]()}catch(k){c=0;for(e=a.h;e;){if((f=b[e+11])&&f.length>0){c=e;break}A(a,e);a.l=e;e=b[e+8];a.h=e}if(!c)throw k;e=b[c+11].pop();f=b[c+9];e.L!==void 0?(b[f+e.L]=k,b[c+2]=e.T):(b[f+e.P]=e.X,b[f+e.W]=k,b[c+2]= +e.R);a.l=c+b[c+1];a.h=c}if(!a.h)return a.g[0]}}var C=u.prototype;C[58675]=function(){var a=this.g,b=a[this.h+9],c=v(this);a[b+c]=z(this)};C[59513]=function(){var a=this.g,b=a[this.h+9],c=v(this);a[b+c]=v(this)};C[9745]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=z(this);if(!(e in this.i))throw new ReferenceError(`${e} is not defined`);a[b+c]=this.i[e]};C[62699]=function(){var a=this.h,b=this.g,c=b[a+9],e=v(this);a=b[a+4].o[v(this)];b[c+e]=a.C?a.F:a.g[a.B]}; +C[63815]=function(){var a=this.h,b=this.g,c=b[a+9],e=v(this);b[c+e]=b[a+3]};C[51108]=function(){var a=this.g,b=a[this.h+9],c=v(this);a[b+c]=a[b+v(this)]};C[58865]=function(){var a=this.g,b=a[this.h+9];this.i[z(this)]=a[b+v(this)]};C[5169]=function(){var a=this.h,b=this.g,c=b[a+9],e=v(this);a=b[a+4].o[e];b=b[c+v(this)];a.C?a.F=b:a.g[a.B]=b};C[9119]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)],f=a[b+v(this)];a[b+c]=e[f]}; +C[21956]=function(){var a=this.g,b=a[this.h+9],c=a[b+v(this)],e=a[b+v(this)];a=a[b+v(this)];Reflect.set(c,e,a)};C[37517]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)],f=a[b+v(this)];a[b+c]=delete e[f]};C[45047]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e+a[b+v(this)]};C[58367]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e-a[b+v(this)]};C[3343]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e*a[b+v(this)]}; +C[15331]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e/a[b+v(this)]};C[37910]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e%a[b+v(this)]};C[54744]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=Math.pow(e,a[b+v(this)])};C[61072]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e&a[b+v(this)]};C[22373]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e|a[b+v(this)]}; +C[49895]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e^a[b+v(this)]};C[24807]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e<>a[b+v(this)]};C[1383]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e>>>a[b+v(this)]};C[6858]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=ea[b+v(this)]};C[49605]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e<=a[b+v(this)]};C[62024]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e>=a[b+v(this)]};C[37588]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e===a[b+v(this)]};C[54099]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e!==a[b+v(this)]}; +C[9938]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e==a[b+v(this)]};C[31693]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e!=a[b+v(this)]};C[58249]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e in a[b+v(this)]};C[55198]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)];a[b+c]=e instanceof a[b+v(this)]};C[24245]=function(){var a=this.g,b=a[this.h+9],c=v(this);a[b+c]=-a[b+v(this)]}; +C[30173]=function(){var a=this.g,b=a[this.h+9],c=v(this);a[b+c]=+a[b+v(this)]};C[24355]=function(){var a=this.g,b=a[this.h+9],c=v(this);a[b+c]=!a[b+v(this)]};C[2075]=function(){var a=this.g,b=a[this.h+9],c=v(this);a[b+c]=~a[b+v(this)]};C[42683]=function(){var a=this.g,b=a[this.h+9],c=v(this);a[b+c]=typeof a[b+v(this)]};C[2210]=function(){var a=this.g,b=a[this.h+9],c=v(this);v(this);a[b+c]=void 0}; +C[31433]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=z(this);e=Object.prototype.hasOwnProperty.call(this.i,e)?this.i[e]:void 0;a[b+c]=typeof e};C[1248]=function(){this.g[this.h+2]=v(this)};C[62628]=function(){var a=this.h,b=this.g,c=b[a+9],e=v(this),f=v(this);b[c+e]||(b[a+2]=f)};C[51402]=function(){var a=this.h,b=this.g,c=b[a+9],e=v(this),f=v(this);b[c+e]&&(b[a+2]=f)}; +C[31339]=function(){var a=this.g,b=a[this.h+9],c=v(this),e=a[b+v(this)],f=v(this);if(f===1638052255)var h=a[b+v(this)];else{h=Array(f);for(var k=0;k>1):0]=c}; +C[31958]=function(){var a=this.g;throw a[a[this.h+9]+v(this)];}; +C[3126]=function(){for(var a=this.h,b=this.g,c=b[a+9],e=v(this),f=v(this),h=v(this),k=v(this),l=v(this),q=v(this),p=Array(l),m=0;m=f.J.length?b[a+2]=h:b[c+e]=f.J[f.M++]}; +C[2373]=function(){var a=this.h,b=this.g,c=b[a+11];c||(b[a+11]=c=[]);c.push({T:v(this),L:v(this)})};C[56388]=function(){this.g[this.h+11].pop()};C[4416]=function(){var a=this.h,b=this.g,c=b[a+11];c||(b[a+11]=c=[]);c.push({R:v(this),P:v(this),W:v(this),X:v(this)})};C[43666]=function(){for(var a=v(this),b=v(this),c=v(this),e=v(this)^a|0,f=b;f>>13)>>>0};C[30352]=function(){var a=this.h,b=this.g;b[a+2]=b[b[a+9]+v(this)]};C[30171]=function(){debugger}; +var D=globalThis;typeof window!=="undefined"&&(D.window=window,D.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(D.module=module,D.exports=typeof exports!=="undefined"?exports:void 0); +B(new u([63815,1,3126,3,39,1,7,0,0,51108,2,3,9745,3,0,0,58675,4,1,0,58675,5,2,0,31339,6,2,1,5,21956,3,4,6,58675,3,3,0,10097,3,63815,2,58675,5,4,0,51108,3,5,58675,5,4,0,51108,4,5,6858,5,4,0,62628,5,85,45047,5,3,4,51108,3,5,51108,5,4,58675,5,5,0,45047,6,4,5,51108,4,6,1248,55,10097,3,58675,5,3,0,10097,5],["window","TEST_OUTPUT",4,void 0,0,1],D),new t({m:0,v:7,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-specialized.encoded.js b/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-specialized.encoded.js new file mode 100644 index 00000000..f8aa8473 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-layers/minify-table-specialized.encoded.js @@ -0,0 +1,26 @@ +var e={};function f(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}var m=new WeakMap;function r(a,b){this.h=a;this.B=b;this.C=!1;this.F=void 0}function t(a){this.U=a;this.o=[];this.prototype={}}function u(a,b,c){this.l=a;this.K=b;this.i=c;this.D=null;this.h=[];this.j=1;this.g=0}function v(a){return a.l[a.h[a.g+0]++]} +function w(a,b,c,d,g){for(var k=b.U,h=a.h,l=a.j,q=8+k.v,p=l+q;h.length>>13)&65535);return b}function A(a,b){var c=a.D;if(c){var d=a.h;a=b+d[b+6];for(b=d[b+7];b=d.length)break;b[c+0]=g+1;var k=a.l[g];typeof e.u==="number"&&(b[c+e.u]=(b[c+e.u]+1)%d.length);typeof e.A==="number"&&b[c+e.A]++;typeof e.P==="number"&&(b[c+e.P]=k);typeof e.H==="number"&&(b[c+e.H]=b[c+e.H]^g|0);try{a[k]()}catch(h){c=0;for(d=a.g;d;){if((g=b[d+5])&&g.length>0){c=d;break}A(a,d);a.j=d;d=b[d+1];a.g=d}if(!c)throw h;d=b[c+5].pop();g=b[c+7];d.L!==void 0?(b[g+d.L]=h,b[c+0]=d.V):(b[g+d.S]=d.Z,b[g+d.Y]=h,b[c+0]=d.T); +a.j=c+b[c+6];a.g=c}if(!a.g)return a.h[0]}}var C=u.prototype;C[0]=function(){var a=this.h,b=a[this.g+7],c=v(this);a[b+c]=z(this)};C[1]=function(){var a=this.h,b=a[this.g+7],c=v(this);a[b+c]=v(this)};C[2]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=z(this);if(!(d in this.i))throw new ReferenceError(`${d} is not defined`);a[b+c]=this.i[d]};C[3]=function(){var a=this.g,b=this.h,c=b[a+7],d=v(this);a=b[a+4].o[v(this)];b[c+d]=a.C?a.F:a.h[a.B]}; +C[4]=function(){var a=this.g,b=this.h,c=b[a+7],d=v(this);b[c+d]=b[a+3]};C[5]=function(){var a=this.h,b=a[this.g+7],c=v(this);a[b+c]=a[b+v(this)]};C[6]=function(){var a=this.h,b=a[this.g+7];this.i[z(this)]=a[b+v(this)]};C[7]=function(){var a=this.g,b=this.h,c=b[a+7],d=v(this);a=b[a+4].o[d];b=b[c+v(this)];a.C?a.F=b:a.h[a.B]=b};C[8]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)],g=a[b+v(this)];a[b+c]=d[g]}; +C[9]=function(){var a=this.h,b=a[this.g+7],c=a[b+v(this)],d=a[b+v(this)];a=a[b+v(this)];Reflect.set(c,d,a)};C[10]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)],g=a[b+v(this)];a[b+c]=delete d[g]};C[11]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d+a[b+v(this)]};C[12]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d-a[b+v(this)]};C[13]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d*a[b+v(this)]}; +C[14]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d/a[b+v(this)]};C[15]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d%a[b+v(this)]};C[60]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=Math.pow(d,a[b+v(this)])};C[16]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d&a[b+v(this)]};C[17]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d|a[b+v(this)]}; +C[18]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d^a[b+v(this)]};C[19]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d<>a[b+v(this)]};C[21]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d>>>a[b+v(this)]};C[22]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=da[b+v(this)]};C[24]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d<=a[b+v(this)]};C[25]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d>=a[b+v(this)]};C[26]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d===a[b+v(this)]};C[27]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d!==a[b+v(this)]}; +C[28]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d==a[b+v(this)]};C[29]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d!=a[b+v(this)]};C[30]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d in a[b+v(this)]};C[31]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)];a[b+c]=d instanceof a[b+v(this)]};C[32]=function(){var a=this.h,b=a[this.g+7],c=v(this);a[b+c]=-a[b+v(this)]}; +C[33]=function(){var a=this.h,b=a[this.g+7],c=v(this);a[b+c]=+a[b+v(this)]};C[34]=function(){var a=this.h,b=a[this.g+7],c=v(this);a[b+c]=!a[b+v(this)]};C[35]=function(){var a=this.h,b=a[this.g+7],c=v(this);a[b+c]=~a[b+v(this)]};C[36]=function(){var a=this.h,b=a[this.g+7],c=v(this);a[b+c]=typeof a[b+v(this)]};C[37]=function(){var a=this.h,b=a[this.g+7],c=v(this);v(this);a[b+c]=void 0}; +C[38]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=z(this);d=Object.prototype.hasOwnProperty.call(this.i,d)?this.i[d]:void 0;a[b+c]=typeof d};C[39]=function(){this.h[this.g+0]=v(this)};C[40]=function(){var a=this.g,b=this.h,c=b[a+7],d=v(this),g=v(this);b[c+d]||(b[a+0]=g)};C[41]=function(){var a=this.g,b=this.h,c=b[a+7],d=v(this),g=v(this);b[c+d]&&(b[a+0]=g)}; +C[42]=function(){var a=this.h,b=a[this.g+7],c=v(this),d=a[b+v(this)],g=v(this);if(g===65535)var k=a[b+v(this)];else{k=Array(g);for(var h=0;h>1):0]=c}; +C[46]=function(){var a=this.h;throw a[a[this.g+7]+v(this)];}; +C[47]=function(){for(var a=this.g,b=this.h,c=b[a+7],d=v(this),g=v(this),k=v(this),h=v(this),l=v(this),q=v(this),p=Array(l),n=0;n=g.J.length?b[a+0]=k:b[c+d]=g.J[g.M++]}; +C[54]=function(){var a=this.g,b=this.h,c=b[a+5];c||(b[a+5]=c=[]);c.push({V:v(this),L:v(this)})};C[55]=function(){this.h[this.g+5].pop()};C[59]=function(){var a=this.g,b=this.h,c=b[a+5];c||(b[a+5]=c=[]);c.push({T:v(this),S:v(this),Y:v(this),Z:v(this)})};C[56]=function(){for(var a=v(this),b=v(this),c=v(this),d=v(this)^a|0,g=b;g>>13)>>>0};C[58]=function(){var a=this.g,b=this.h;b[a+0]=b[b[a+7]+v(this)]};C[57]=function(){debugger}; +C[61]=function(){var a=this.g,b=this.h;b[b[a+7]+1]=b[a+3]};C[62]=function(){var a=this.h,b=a[this.g+7];a[b+2]=a[b+3]};C[63]=function(){var a=this.h,b=a[this.g+7],c=z(this,0,0);if(!(c in this.i))throw new ReferenceError(`${c} is not defined`);a[b+3]=this.i[c]};C[64]=function(){var a=this.h;a[a[this.g+7]+4]=z(this,1,0)};C[65]=function(){var a=this.h;a[a[this.g+7]+5]=z(this,2,0)};C[66]=function(){var a=this.h,b=a[this.g+7];Reflect.set(a[b+3],a[b+4],a[b+6])}; +C[67]=function(){var a=this.h;a[a[this.g+7]+3]=z(this,3,0)};C[68]=function(){var a=this.g,b=this.h,c=b[b[a+7]+3];A(this,a);var d=b[a+1],g=b[a+2];g&1&&(typeof c!=="object"||c===null)&&(c=b[a+3]);for(var k=a+b[a+6],h=a;h>1):0]=c};C[69]=function(){var a=this.g,b=this.h;b[b[a+7]+2]=b[a+3]};C[70]=function(){var a=this.h;a[a[this.g+7]+5]=z(this,4,0)};C[71]=function(){var a=this.h,b=a[this.g+7];a[b+3]=a[b+5]}; +C[72]=function(){var a=this.h,b=a[this.g+7];a[b+4]=a[b+5]};C[73]=function(){var a=this.h,b=a[this.g+7];a[b+5]=a[b+4]>1):0]=c};C[80]=function(){var a=this.g,b=this.h,c=b[b[a+7]+5];A(this,a);var d=b[a+1],g=b[a+2];g&1&&(typeof c!=="object"||c===null)&&(c=b[a+3]);for(var k=a+b[a+6],h=a;h>1):0]=c};var D=globalThis; +typeof window!=="undefined"&&(D.window=window,D.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(D.module=module,D.exports=typeof exports!=="undefined"?exports:void 0);B(new u([61,47,3,20,1,7,0,0,62,63,64,65,42,6,2,1,5,66,67,68,69,70,71,0,5,4,0,72,73,74,75,76,5,5,4,0,5,5,0,77,5,4,6,78,79,0,5,3,0,80],["window","TEST_OUTPUT",4,void 0,0,1],D),new t({m:0,v:7,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-layers/source.js b/test/vm/jsconfuser-vm/fixtures/runtime-layers/source.js new file mode 100644 index 00000000..10714cdd --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-layers/source.js @@ -0,0 +1,2 @@ +function f(a) { var s = 0; for (var i = 0; i < a; i++) s += i; return s; } +window.TEST_OUTPUT = f(4); diff --git a/test/vm/jsconfuser-vm/identify-handlers.test.js b/test/vm/jsconfuser-vm/identify-handlers.test.js new file mode 100644 index 00000000..d94d88fa --- /dev/null +++ b/test/vm/jsconfuser-vm/identify-handlers.test.js @@ -0,0 +1,190 @@ +import fs from 'node:fs' +import { parse } from '@babel/parser' +import { expect, test } from 'vitest' +import { PARSER_OPTIONS } from '../../../src/vm/jsconfuser-vm/container-role-helpers.js' +import { + locateHandlers, + normalizeStatements, +} from '../../../src/vm/jsconfuser-vm/locate-handlers.js' +import PluginJsconfuser from '../../../src/plugin/jsconfuser.js' +import normalizeVmInput from '../../../src/visitor/jsconfuser-vm/normalize-source.js' +import { + behaviourTable, + requireIdentifiedHandlers, +} from '../../../src/vm/jsconfuser-vm/identify-handlers.js' +import { + handlerEffects, + templateEffects, + unifyEffects, +} from '../../../src/vm/jsconfuser-vm/handler-effects.js' + +// The runtime-layers minify-table fixtures: Closure ADVANCED minify over a handler table, with one +// added-opcode option each (generation.json). Expected values are the encoder's own: re-encoding the +// same source, options and seed without minify names every added handler in a comment. +const fixture = (name) => + parse( + fs.readFileSync( + new URL( + `fixtures/runtime-layers/minify-table-${name}.encoded.js`, + import.meta.url, + ), + 'utf8', + ), + PARSER_OPTIONS, + ) +const identified = (name) => requireIdentifiedHandlers(fixture(name), 'test') + +// The outer-layers fixtures: one randomized VM layer under one outer js-confuser option each +// (generation.json). Every outer option keeps behaviour and re-spells handlers; the expected values +// are the VM layer's own OP and SENTINELS objects. +const outerFixture = (name) => + fs.readFileSync( + new URL(`fixtures/outer-layers/${name}.encoded.js`, import.meta.url), + 'utf8', + ) +const encoderObject = (source, name) => { + const declarator = parse(source, PARSER_OPTIONS) + .program.body.flatMap((statement) => + statement.type === 'VariableDeclaration' ? statement.declarations : [], + ) + .find((item) => item.id.name === name) + return Object.fromEntries( + declarator.init.properties.map((item) => [item.key.name, item.value.value]), + ) +} +const outerPrepared = (name) => + parse(normalizeVmInput(PluginJsconfuser(outerFixture(name))), PARSER_OPTIONS) + +test.each([ + [ + 'beside their specialized copies', + () => fixture('specialized'), + { LOAD_THIS: 4, ADD: 11 }, + undefined, + ], + ...[ + 'compact', + 'object-extraction', + 'moved-declarations', + 'minify', + 'variable-masking', + ].map((name) => [ + `under the outer ${name} layer`, + () => outerPrepared(name), + encoderObject(outerFixture('vm'), 'OP'), + encoderObject(outerFixture('vm'), 'SENTINELS').CALL_SPREAD, + ]), +])( + 'names all 61 canonical handlers by behaviour %s', + (_, program, expected, sentinel) => { + const view = locateHandlers(program()) + const { opcodes, named, sentinel: found } = behaviourTable(view) + expect( + Object.values(opcodes).filter((value) => value !== null), + ).toHaveLength(61) + expect(named.size).toBe(61) + expect(opcodes).toMatchObject(expected) + if (sentinel !== undefined) expect(found).toBe(sentinel) + }, +) + +test('a specialized handler is one canonical handler with its operands baked in', () => { + const { added } = identified('specialized') + expect(added.get(75)).toEqual({ + kind: 'specialized', + name: 'ADD', + opcode: 11, + operands: [5, 3, 4], + }) + expect(added.get(63)).toMatchObject({ + name: 'LOAD_GLOBAL', + operands: [3, 0, 0], + }) + expect(added.get(74)).toMatchObject({ + name: 'JUMP_IF_FALSE', + operands: [5, 44], + }) + expect( + [...added.values()].every((item) => item?.kind === 'specialized'), + ).toBe(true) +}) + +test('an alias is one canonical handler reading its operands in another order', () => { + const { added } = identified('aliased') + expect(added.get(61)).toMatchObject({ + kind: 'alias', + name: 'LOAD_CONST', + canonicalToStored: [2, 1, 0], + }) + // An identity-order alias does what its canonical handler does; it is still the alias. + expect(added.get(64)).toMatchObject({ + kind: 'alias', + name: 'LOAD_THIS', + canonicalToStored: [0], + }) + expect(added.size).toBe(10) +}) + +test('a macro is canonical handlers in sequence, operands read in order', () => { + const { added } = identified('macro') + const steps = (label) => added.get(label).steps.map((step) => step.name) + expect(steps(61)).toEqual(['LOAD_CONST', 'RETURN']) + expect(steps(63)).toEqual(['MOVE', 'LOAD_CONST']) + expect(added.get(64)).toMatchObject({ + kind: 'macro', + steps: [ + { name: 'ADD', reads: [0, 1, 2] }, + { name: 'MOVE', reads: [3, 4] }, + ], + }) +}) + +test('a synthetic anti case reads every operand up front, in its stored permutation', () => { + const found = identified('anti').added.get(61) + expect(found.kind).toBe('anti') + expect(found.steps.map((step) => step.name)).toEqual([ + 'JUMP_IF_FALSE', + 'EXP', + 'MOVE', + 'BUILD_OBJECT', + 'BUILD_ARRAY', + ]) + // The encoder's prelude: `_operands = [_unsorted[9], _unsorted[4], _unsorted[8], ...]`. + expect(found.steps.flatMap((step) => step.reads)).toEqual([ + 9, 4, 8, 3, 13, 6, 5, 12, 11, 10, 0, 1, 2, 7, + ]) +}) + +test('effects compare across minify spellings: a folded literal fills an operand slot', () => { + const view = locateHandlers(fixture('specialized')) + const handler = (label) => view.handlers.find((item) => item.label === label) + const identities = new Map() + const add = handlerEffects(view, handler(11), identities) + const baked = handlerEffects(view, handler(75), identities) + // Minify kept ADD's operand locals and folded ADD_5_3_4's literals into one statement. + expect(add.operands).toBe(3) + expect(baked.operands).toBe(0) + const slots = new Map() + expect(unifyEffects(add.effects, baked.effects, slots)).toBe(true) + expect([...slots]).toEqual([ + [0, { literal: 5 }], + [1, { literal: 3 }], + [2, { literal: 4 }], + ]) + // A different canonical handler does not fit. + expect( + unifyEffects( + handlerEffects(view, handler(12), identities).effects, + baked.effects, + ), + ).toBe(false) // Closure writes a specialized BAND whose destination is its left operand as `a[b+85] &= a[b+84]` + // (corpus VM-GLM-1): a compound bitwise write and the written-out chain are one term. + const effects = (source) => + templateEffects( + normalizeStatements(parse(source, PARSER_OPTIONS).program.body), + ).effects + expect(effects('$regs[$base + 85] &= $regs[$base + 84];')).toEqual( + effects('$regs[$base + 85] = $regs[$base + 85] & $regs[$base + 84];'), + ) + expect(effects('$x = $a ^ ($b ^ $c);')).toEqual(effects('$x = $a ^ $b ^ $c;')) +}) diff --git a/test/vm/jsconfuser-vm/partition-functions.test.js b/test/vm/jsconfuser-vm/partition-functions.test.js new file mode 100644 index 00000000..776298d9 --- /dev/null +++ b/test/vm/jsconfuser-vm/partition-functions.test.js @@ -0,0 +1,265 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { extractContainerFromSource } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { validateReferences } from '../../../src/vm/jsconfuser-vm/validate-references.js' +import { + diagnoseFunctionPartition, + partitionFunctions, +} from '../../../src/vm/jsconfuser-vm/partition-functions.js' + +const repositoryRoot = new URL('../../../', import.meta.url) + +function sourceAt(relativePath) { + return fs.readFileSync(new URL(relativePath, repositoryRoot), 'utf8') +} + +function analysesAt(relativePath) { + const container = extractContainerFromSource(sourceAt(relativePath)) + expect(container, `extract-container declined ${relativePath}`).not.toBeNull() + const wordcode = readWordcode(container) + expect(wordcode, `read-wordcode declined ${relativePath}`).not.toBeNull() + const references = validateReferences(container, wordcode) + expect( + references, + `validate-references declined ${relativePath}`, + ).not.toBeNull() + return { container, wordcode, references } +} + +function partitionAt(relativePath) { + const analyses = analysesAt(relativePath) + const diagnosis = diagnoseFunctionPartition( + analyses.container, + analyses.wordcode, + analyses.references, + ) + expect( + diagnosis.ok, + `${relativePath}: ${diagnosis.diagnostic?.message}`, + ).toBe(true) + return { ...analyses, result: diagnosis.result } +} + +function deepFrozen(value) { + if (!value || typeof value !== 'object') return true + return ( + Object.isFrozen(value) && + Object.values(value).every((child) => deepFrozen(child)) + ) +} + +function corpusPaths() { + return ['raw', 'focused'].flatMap((kind) => { + const root = new URL( + `test/vm/jsconfuser-vm/fixtures/corpus/${kind}/`, + repositoryRoot, + ) + return fs + .readdirSync(root, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map( + (entry) => + `test/vm/jsconfuser-vm/fixtures/corpus/${kind}/${entry.name}/encoded.js`, + ) + .filter((relativePath) => + fs.existsSync(new URL(relativePath, repositoryRoot)), + ) + .sort() + }) +} + +test('partitions the reference stream into one immutable root function', () => { + const analyses = partitionAt( + 'test/vm/jsconfuser-vm/fixtures/reference/encoded.js', + ) + + expect(analyses.result).toMatchObject({ + schemaVersion: 'jsconfuser-vm-functions.v1', + encoding: 'numeric-u32', + wordCount: 32, + instructionCount: 9, + entryPcs: [0], + functions: [ + { + id: 0, + kind: 'root', + startPc: 0, + endPc: 32, + paramCount: 0, + regCount: 7, + captureCount: 0, + hasRest: false, + parentFunctionId: null, + instructionCount: 9, + descriptor: null, + closureSites: [], + }, + ], + ownership: analyses.wordcode.instructions.map((instruction) => ({ + pc: instruction.pc, + functionId: 0, + })), + labels: [], + frame: { + frameStart: 1, + headerSize: 8, + root: { functionId: 0, startPc: 0, regCount: 7 }, + }, + }) + expect(deepFrozen(analyses.result)).toBe(true) + expect( + partitionFunctions( + analyses.container, + analyses.wordcode, + analyses.references, + ), + ).toEqual(analyses.result) +}) + +test('partitions nested closures and records unique parent/child creation sites', () => { + const analyses = partitionAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/encoded.js', + ) + + expect(analyses.result.entryPcs).toEqual([0, 55, 81]) + expect( + analyses.result.functions.map( + ({ id, kind, startPc, endPc, parentFunctionId, instructionCount }) => ({ + id, + kind, + startPc, + endPc, + parentFunctionId, + instructionCount, + }), + ), + ).toEqual([ + { + id: 0, + kind: 'root', + startPc: 0, + endPc: 55, + parentFunctionId: null, + instructionCount: 14, + }, + { + id: 1, + kind: 'closure', + startPc: 55, + endPc: 81, + parentFunctionId: 0, + instructionCount: 7, + }, + { + id: 2, + kind: 'closure', + startPc: 81, + endPc: 111, + parentFunctionId: 1, + instructionCount: 10, + }, + ]) + expect(analyses.result.functions[0].closureSites).toMatchObject([ + { + creationPc: 2, + parentFunctionId: 0, + childFunctionId: 1, + childStartPc: 55, + destinationRegister: 4, + }, + ]) + expect(analyses.result.functions[1].closureSites).toMatchObject([ + { + creationPc: 64, + parentFunctionId: 1, + childFunctionId: 2, + childStartPc: 81, + destinationRegister: 3, + }, + ]) + expect(analyses.result.functions[2].closureSites).toEqual([]) + + const ownedPcs = analyses.result.functions.flatMap( + ({ instructionPcs }) => instructionPcs, + ) + expect(ownedPcs).toEqual(analyses.wordcode.instructions.map(({ pc }) => pc)) + expect(analyses.result.ownership).toEqual( + analyses.wordcode.instructions.map(({ pc }) => ({ + pc, + functionId: analyses.result.functions.find(({ instructionPcs }) => + instructionPcs.includes(pc), + ).id, + })), + ) +}) + +test('keeps exceptional labels local while partitioning multiple closures', () => { + const analyses = partitionAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/encoded.js', + ) + + expect(analyses.result.entryPcs).toEqual([0, 102, 182, 255, 372]) + expect( + analyses.result.functions.map(({ startPc, endPc }) => [startPc, endPc]), + ).toEqual([ + [0, 102], + [102, 182], + [182, 255], + [255, 372], + [372, 517], + ]) + expect( + analyses.result.functions.map(({ closureSites }) => closureSites.length), + ).toEqual([4, 0, 0, 0, 0]) + expect( + analyses.result.labels + .filter(({ role }) => role !== 'functionEntry') + .every( + ({ sourceFunctionId, targetFunctionId }) => + sourceFunctionId === targetFunctionId, + ), + ).toBe(true) +}) + +test('partitions every tracked stripped raw and focused corpus input', () => { + const paths = corpusPaths() + expect(paths).toHaveLength(37) + for (const relativePath of paths) { + const analyses = analysesAt(relativePath) + const diagnosis = diagnoseFunctionPartition( + analyses.container, + analyses.wordcode, + analyses.references, + ) + expect( + diagnosis.ok, + `${relativePath}: ${diagnosis.diagnostic?.message}`, + ).toBe(true) + expect(diagnosis.result.entryPcs[0]).toBe(0) + expect(diagnosis.result.functions.at(-1).endPc).toBe( + diagnosis.result.wordCount, + ) + expect(diagnosis.result.ownership).toHaveLength( + diagnosis.result.instructionCount, + ) + } +}) + +test('does not mutate accepted frozen predecessor records', () => { + const analyses = analysesAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/encoded.js', + ) + const before = structuredClone(analyses) + const result = partitionFunctions( + analyses.container, + analyses.wordcode, + analyses.references, + ) + + expect(result).not.toBeNull() + expect(analyses).toEqual(before) + expect(deepFrozen(result)).toBe(true) + expect(() => result.functions.push({})).toThrow() + expect(() => (result.functions[0].startPc = 99)).toThrow() +}) diff --git a/test/vm/jsconfuser-vm/read-wordcode.test.js b/test/vm/jsconfuser-vm/read-wordcode.test.js new file mode 100644 index 00000000..9e47b575 --- /dev/null +++ b/test/vm/jsconfuser-vm/read-wordcode.test.js @@ -0,0 +1,337 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { extractContainerFromSource } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { + diagnoseWordcode, + readWordcode, +} from '../../../src/vm/jsconfuser-vm/read-wordcode.js' + +const repositoryRoot = new URL('../../../', import.meta.url) + +function sourceAt(relativePath) { + return fs.readFileSync(new URL(relativePath, repositoryRoot), 'utf8') +} + +function containerAt(relativePath) { + const container = extractContainerFromSource(sourceAt(relativePath)) + expect(container, `extract-container declined ${relativePath}`).not.toBeNull() + return container +} + +function withWords(container, words) { + const copy = structuredClone(container) + copy.roles.words.values = [...words] + return copy +} + +function deepFrozen(value) { + if (!value || typeof value !== 'object') return true + return ( + Object.isFrozen(value) && + Object.values(value).every((child) => deepFrozen(child)) + ) +} + +function instructionsAt(relativePath) { + const result = readWordcode(containerAt(relativePath)) + expect(result).not.toBeNull() + return result.instructions +} + +test('parses the reference stream with exact PCs and complete consumption', () => { + const container = containerAt( + 'test/vm/jsconfuser-vm/fixtures/reference/encoded.js', + ) + const result = readWordcode(container) + + expect(result).toMatchObject({ + schemaVersion: 'jsconfuser-vm-wordcode.v1', + encoding: 'numeric-u32', + wordCount: 32, + instructionCount: 9, + consumedWords: 32, + nextPc: 32, + }) + expect(result.instructions.map(({ name }) => name)).toEqual([ + 'LOAD_THIS', + 'LOAD_GLOBAL', + 'LOAD_CONST', + 'LOAD_CONST', + 'LOAD_CONST', + 'ADD', + 'SET_PROP', + 'LOAD_CONST', + 'RETURN', + ]) + expect( + result.instructions.every( + (instruction) => + instruction.nextPc === instruction.pc + instruction.width, + ), + ).toBe(true) + expect(result.instructions.at(-1).nextPc).toBe(result.wordCount) + expect(deepFrozen(result)).toBe(true) +}) + +test('detector-first assertions prove every target-local variable form is observed', () => { + const targetCases = [ + [ + 'test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-store-global-assignment/encoded.js', + 'STORE_GLOBAL', + ], + [ + 'test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-unary-positive-coercion/encoded.js', + 'UNARY_POS', + ], + [ + 'test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/encoded.js', + 'CALL_METHOD', + ], + [ + 'test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/encoded.js', + 'NEW', + ], + [ + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/encoded.js', + 'CALL', + ], + [ + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/encoded.js', + 'MAKE_CLOSURE', + ], + ] + + for (const [path, opcode] of targetCases) { + const target = instructionsAt(path).filter(({ name }) => name === opcode) + expect(target.length, `${path} detector`).toBeGreaterThan(0) + } +}) + +test('parses fixed and spread forms for CALL, CALL_METHOD, and NEW', () => { + const callInstructions = instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-spread-order/encoded.js', + ) + const methodInstructions = instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/encoded.js', + ) + const newInstructions = instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/encoded.js', + ) + const fixedCall = instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-arrow-forms/encoded.js', + ).find(({ name }) => name === 'CALL') + const fixedMethod = methodInstructions.find( + ({ name, form }) => name === 'CALL_METHOD' && form?.kind === 'fixed', + ) + const fixedNew = instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-for-in-enumeration/encoded.js', + ).find(({ name }) => name === 'NEW') + const spreadCall = callInstructions.find( + ({ name, form }) => name === 'CALL' && form?.kind === 'spread', + ) + const spreadMethod = methodInstructions.find( + ({ form }) => form?.kind === 'spread', + ) + const spreadNew = newInstructions.find(({ form }) => form?.kind === 'spread') + + expect(fixedCall).toMatchObject({ + form: { kind: 'fixed', argc: 0, sentinel: null }, + arguments: { kind: 'fixed', count: 0, registers: [] }, + }) + expect(fixedMethod).toMatchObject({ + form: { kind: 'fixed', argc: 1, sentinel: null }, + arguments: { kind: 'fixed', count: 1 }, + }) + expect(fixedNew).toMatchObject({ + form: { kind: 'fixed', argc: 0, sentinel: null }, + arguments: { kind: 'fixed', count: 0, registers: [] }, + }) + expect(spreadCall).toMatchObject({ + words: [42, 8, 2, 65535, 7], + width: 5, + nextPc: 142, + form: { + kind: 'spread', + argc: 65535, + sentinel: 65535, + arrayRegister: { kind: 'register', index: 7 }, + }, + arguments: { + kind: 'spread', + sentinel: 65535, + arrayRegister: { kind: 'register', index: 7 }, + }, + }) + expect(spreadMethod).toMatchObject({ + words: [43, 4, 2, 5, 65535, 6], + width: 6, + form: { + kind: 'spread', + arrayRegister: { kind: 'register', index: 6 }, + }, + }) + expect(spreadNew).toMatchObject({ + words: [44, 5, 2, 65535, 6], + width: 5, + form: { + kind: 'spread', + arrayRegister: { kind: 'register', index: 6 }, + }, + }) +}) + +test('retains typed closure, collection, zero-width, and EXP records', () => { + const closure = instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-closures-state/encoded.js', + ).find(({ captures }) => captures?.length > 0) + expect(closure).toMatchObject({ + name: 'MAKE_CLOSURE', + width: 9, + functionMeta: { + startPc: 81, + paramCount: 0, + regCount: 5, + captureCount: 1, + hasRest: false, + }, + captures: [{ kind: 'local', index: 2, isLocal: true }], + }) + + const focusedNew = instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-new-spread/encoded.js', + ) + expect(focusedNew.find(({ name }) => name === 'BUILD_ARRAY')).toMatchObject({ + form: { kind: 'array', count: 2 }, + elementRegisters: [ + { kind: 'register', index: 5 }, + { kind: 'register', index: 6 }, + ], + }) + expect( + instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/encoded.js', + ).find(({ name, pairCount }) => name === 'BUILD_OBJECT' && pairCount === 2), + ).toMatchObject({ + form: { kind: 'object', pairCount: 2 }, + pairs: [ + { + key: { kind: 'register', index: 19 }, + value: { kind: 'register', index: 20 }, + }, + { + key: { kind: 'register', index: 21 }, + value: { kind: 'register', index: 23 }, + }, + ], + }) + + const finallyInstructions = instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/encoded.js', + ) + expect( + finallyInstructions.find(({ name }) => name === 'TRY_END'), + ).toMatchObject({ + name: 'TRY_END', + operands: [], + width: 1, + }) + expect( + instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-debugger-order/encoded.js', + ).find(({ name }) => name === 'DEBUGGER'), + ).toMatchObject({ + name: 'DEBUGGER', + operands: [], + width: 1, + }) + const exp = instructionsAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-expression-values/encoded.js', + ).find(({ name }) => name === 'EXP') + expect(exp).toMatchObject({ + name: 'EXP', + opcode: { name: 'EXP', value: 60 }, + operands: [ + { kind: 'register' }, + { kind: 'register' }, + { kind: 'register' }, + ], + width: 4, + }) +}) + +test.each([ + ['fixed operand', [4], 'truncated-instruction'], + ['spread call array register', [42, 0, 1, 65535], 'truncated-instruction'], + ['closure capture pair', [47, 0, 0, 0, 0, 1, 0, 1], 'truncated-instruction'], + ['array count payload', [48, 0, 2, 0], 'truncated-instruction'], + ['object count payload', [49, 0, 1, 0], 'truncated-instruction'], +])( + 'declines truncated %s before returning partial records', + (_label, words, code) => { + const container = containerAt( + 'test/vm/jsconfuser-vm/fixtures/reference/encoded.js', + ) + const report = diagnoseWordcode(withWords(container, words)) + + expect(report.ok).toBe(false) + expect(report.result).toBeNull() + expect(report.diagnostic.code).toBe(code) + }, +) + +test('declines unknown, extra/trailing, and hardening words', () => { + const container = containerAt( + 'test/vm/jsconfuser-vm/fixtures/reference/encoded.js', + ) + const words = container.roles.words.values + + expect(diagnoseWordcode(withWords(container, [61])).diagnostic.code).toBe( + 'unknown-opcode', + ) + expect( + diagnoseWordcode(withWords(container, [...words, 61])).diagnostic.code, + ).toBe('unknown-opcode') + expect(diagnoseWordcode(withWords(container, [56])).diagnostic.code).toBe( + 'unsupported-hardening-opcode', + ) +}) + +test('preserves the container input and freezes the parsed wordcode', () => { + const container = containerAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/encoded.js', + ) + const before = JSON.stringify(container) + const result = readWordcode(container) + + expect(JSON.stringify(container)).toBe(before) + expect(result.words).not.toBe(container.roles.words.values) + expect(deepFrozen(result)).toBe(true) + expect(() => result.words.push(1)).toThrow() + expect(() => (result.instructions[0].pc = 99)).toThrow() +}) + +test('parses the stripped numeric raw and focused corpus without comments or target execution', () => { + const roots = [ + 'test/vm/jsconfuser-vm/fixtures/corpus/raw', + 'test/vm/jsconfuser-vm/fixtures/corpus/focused', + ] + let cellCount = 0 + for (const root of roots) { + const rootUrl = new URL(`${root}/`, repositoryRoot) + for (const entry of fs.readdirSync(rootUrl).sort()) { + const entryUrl = new URL(entry, rootUrl) + if (!fs.statSync(entryUrl).isDirectory()) continue + const cell = new URL(`${entry}/`, rootUrl) + const encoded = new URL('encoded.js', cell) + if (!fs.existsSync(encoded)) continue + const result = readWordcode( + extractContainerFromSource(fs.readFileSync(encoded, 'utf8')), + ) + expect(result, `${root}/${entry}`).not.toBeNull() + expect(result.consumedWords).toBe(result.wordCount) + expect(result.instructions.at(-1).nextPc).toBe(result.wordCount) + cellCount += 1 + } + } + expect(cellCount).toBe(37) +}) diff --git a/test/vm/jsconfuser-vm/validate-references.test.js b/test/vm/jsconfuser-vm/validate-references.test.js new file mode 100644 index 00000000..60b5a263 --- /dev/null +++ b/test/vm/jsconfuser-vm/validate-references.test.js @@ -0,0 +1,119 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { extractContainerFromSource } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { + diagnoseReferences, + validateReferences, +} from '../../../src/vm/jsconfuser-vm/validate-references.js' + +const repositoryRoot = new URL('../../../', import.meta.url) + +function sourceAt(relativePath) { + return fs.readFileSync(new URL(relativePath, repositoryRoot), 'utf8') +} + +function analysesAt(relativePath) { + const container = extractContainerFromSource(sourceAt(relativePath)) + expect(container, `extract-container declined ${relativePath}`).not.toBeNull() + const wordcode = readWordcode(container) + expect(wordcode, `read-wordcode declined ${relativePath}`).not.toBeNull() + return { container, wordcode } +} + +function deepFrozen(value) { + if (!value || typeof value !== 'object') return true + return ( + Object.isFrozen(value) && + Object.values(value).every((child) => deepFrozen(child)) + ) +} + +function corpusPaths() { + return ['raw', 'focused'].flatMap((kind) => { + const root = new URL( + `test/vm/jsconfuser-vm/fixtures/corpus/${kind}/`, + repositoryRoot, + ) + return fs + .readdirSync(root, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map( + (entry) => + `test/vm/jsconfuser-vm/fixtures/corpus/${kind}/${entry.name}/encoded.js`, + ) + .filter((relativePath) => + fs.existsSync(new URL(relativePath, repositoryRoot)), + ) + .sort() + }) +} + +test('returns immutable constants, references, labels, and root frame metadata', () => { + const analyses = analysesAt( + 'test/vm/jsconfuser-vm/fixtures/reference/encoded.js', + ) + const diagnosis = diagnoseReferences(analyses.container, analyses.wordcode) + + expect(diagnosis).toMatchObject({ ok: true, diagnostic: null }) + expect(diagnosis.result).toMatchObject({ + schemaVersion: 'jsconfuser-vm-references.v1', + encoding: 'numeric-u32', + wordCount: 32, + instructionCount: 9, + constants: { poolSize: analyses.container.roles.pool.values.length }, + registers: { rootCount: 7, maxCount: 7, maxCaptureCount: 0 }, + frame: { + frameStart: 1, + headerSize: 8, + mainStartPc: 0, + mainRegCount: 7, + root: { + frameBase: 1, + frameSize: 15, + registerBase: 9, + registerWindow: { start: 9, end: 16 }, + frameEnd: 16, + }, + }, + }) + expect(diagnosis.result.frame.descriptors).toEqual([]) + expect(diagnosis.result.constants.values).toEqual( + analyses.container.roles.pool.values, + ) + expect(diagnosis.result.labels.boundaries).toEqual( + analyses.wordcode.instructions.map(({ pc }) => pc), + ) + expect(deepFrozen(diagnosis.result)).toBe(true) + expect(validateReferences(analyses.container, analyses.wordcode)).toEqual( + diagnosis.result, + ) +}) + +test('validates references for every tracked stripped raw and focused corpus input', () => { + const paths = corpusPaths() + expect(paths).toHaveLength(37) + for (const relativePath of paths) { + const analyses = analysesAt(relativePath) + const diagnosis = diagnoseReferences(analyses.container, analyses.wordcode) + expect( + diagnosis.ok, + `${relativePath}: ${diagnosis.diagnostic?.message}`, + ).toBe(true) + } +}) + +test('preserves frozen container and wordcode inputs', () => { + const analyses = analysesAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/focused/focused-call-method-spread/encoded.js', + ) + const before = { + container: structuredClone(analyses.container), + wordcode: structuredClone(analyses.wordcode), + } + const result = validateReferences(analyses.container, analyses.wordcode) + expect(result).not.toBeNull() + expect(analyses.container).toEqual(before.container) + expect(analyses.wordcode).toEqual(before.wordcode) + expect(deepFrozen(result)).toBe(true) +}) From 52c43cabe72fca2388f42bdff07cc6f343bf6172 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 07/12] feat(vm/jsconfuser-vm): normalize the stages added after VM conversion Each option stage after compilation (aliased, specialized, macro and self-modifying opcodes, anti-instrumentation, dispatcher, CFF, string concealing, concealed constants) rewrites the wordcode; each normalizer removes only its own stage's additions, using only that stage's facts, and publishes what it cannot settle as a suspect for the stage that owns it. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- .../analyze-anti-register-flow.js | 500 ++++++++ src/vm/jsconfuser-vm/analyze-anti-runtime.js | 93 ++ .../expand-observed-word-stream.js | 114 ++ src/vm/jsconfuser-vm/finally-continuations.js | 22 + .../normalize-aliased-wordcode.js | 227 ++++ .../jsconfuser-vm/normalize-anti-wordcode.js | 367 ++++++ .../jsconfuser-vm/normalize-cff-wordcode.js | 554 +++++++++ .../normalize-concealed-constants.js | 111 ++ .../normalize-dispatcher-wordcode.js | 205 +++ .../jsconfuser-vm/normalize-macro-wordcode.js | 107 ++ .../normalize-self-modifying-wordcode.js | 339 +++++ .../normalize-specialized-wordcode.js | 216 ++++ .../normalize-string-concealing.js | 564 +++++++++ .../jsconfuser-vm/observe-aliased-runtime.js | 90 ++ .../observe-dispatcher-wordcode.js | 610 +++++++++ src/vm/jsconfuser-vm/observe-macro-runtime.js | 121 ++ .../observe-patch-added-opcodes.js | 88 ++ .../jsconfuser-vm/observe-self-modifying.js | 359 ++++++ .../observe-specialized-runtime.js | 94 ++ .../string-concealing-helper-signatures.js | 81 ++ .../analyze-anti-register-flow.test.js | 144 +++ .../fixtures/anti-runtime-boot/encoded.js | 1094 +++++++++++++++++ .../anti-runtime-boot/qualification.json | 57 + .../fixtures/anti-runtime-boot/source.js | 1 + .../anti-runtime-duplicate-step/encoded.js | 1090 ++++++++++++++++ .../qualification.json | 57 + .../anti-runtime-duplicate-step/source.js | 1 + .../fixtures/self-modifying-boot/encoded.js | 1019 +++++++++++++++ .../self-modifying-boot/qualification.json | 57 + .../fixtures/self-modifying-boot/source.js | 1 + .../self-modifying-closures/encoded.js | 1019 +++++++++++++++ .../qualification.json | 66 + .../self-modifying-closures/source.js | 1 + .../self-modifying-finally/encoded.js | 1019 +++++++++++++++ .../self-modifying-finally/qualification.json | 66 + .../fixtures/self-modifying-finally/source.js | 1 + .../self-modifying-observation.test.js | 158 +++ 37 files changed, 10713 insertions(+) create mode 100644 src/vm/jsconfuser-vm/analyze-anti-register-flow.js create mode 100644 src/vm/jsconfuser-vm/analyze-anti-runtime.js create mode 100644 src/vm/jsconfuser-vm/expand-observed-word-stream.js create mode 100644 src/vm/jsconfuser-vm/finally-continuations.js create mode 100644 src/vm/jsconfuser-vm/normalize-aliased-wordcode.js create mode 100644 src/vm/jsconfuser-vm/normalize-anti-wordcode.js create mode 100644 src/vm/jsconfuser-vm/normalize-cff-wordcode.js create mode 100644 src/vm/jsconfuser-vm/normalize-concealed-constants.js create mode 100644 src/vm/jsconfuser-vm/normalize-dispatcher-wordcode.js create mode 100644 src/vm/jsconfuser-vm/normalize-macro-wordcode.js create mode 100644 src/vm/jsconfuser-vm/normalize-self-modifying-wordcode.js create mode 100644 src/vm/jsconfuser-vm/normalize-specialized-wordcode.js create mode 100644 src/vm/jsconfuser-vm/normalize-string-concealing.js create mode 100644 src/vm/jsconfuser-vm/observe-aliased-runtime.js create mode 100644 src/vm/jsconfuser-vm/observe-dispatcher-wordcode.js create mode 100644 src/vm/jsconfuser-vm/observe-macro-runtime.js create mode 100644 src/vm/jsconfuser-vm/observe-patch-added-opcodes.js create mode 100644 src/vm/jsconfuser-vm/observe-self-modifying.js create mode 100644 src/vm/jsconfuser-vm/observe-specialized-runtime.js create mode 100644 src/vm/jsconfuser-vm/string-concealing-helper-signatures.js create mode 100644 test/vm/jsconfuser-vm/analyze-anti-register-flow.test.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-boot/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-boot/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-boot/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-closures/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-closures/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-closures/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-finally/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-finally/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-finally/source.js create mode 100644 test/vm/jsconfuser-vm/self-modifying-observation.test.js diff --git a/src/vm/jsconfuser-vm/analyze-anti-register-flow.js b/src/vm/jsconfuser-vm/analyze-anti-register-flow.js new file mode 100644 index 00000000..75439dfc --- /dev/null +++ b/src/vm/jsconfuser-vm/analyze-anti-register-flow.js @@ -0,0 +1,500 @@ +import { diagnoseAntiWordStream } from './read-wordcode.js' +import { restoreConcealedValue } from './normalize-concealed-constants.js' + +const REAL_ARITIES = Object.freeze({ + ADD: 3, + SUB: 3, + MUL: 3, + DIV: 3, + MOD: 3, + EXP: 3, + BAND: 3, + BOR: 3, + BXOR: 3, + SHL: 3, + SHR: 3, + USHR: 3, + LT: 3, + GT: 3, + LTE: 3, + GTE: 3, + EQ: 3, + NEQ: 3, + LOOSE_EQ: 3, + LOOSE_NEQ: 3, + UNARY_NEG: 2, + UNARY_POS: 2, + UNARY_NOT: 2, + UNARY_BITNOT: 2, + MOVE: 2, +}) + +// w/r are flowing fake registers; T/F are seeded, never-written anchors. +// c consumes a constant index and conceal key, in that order. +const FAKE_SLOTS = Object.freeze({ + ADD: 'wrr', + SUB: 'wrr', + MUL: 'wrr', + DIV: 'wrr', + MOD: 'wrr', + EXP: 'wrr', + BAND: 'wrr', + BOR: 'wrr', + BXOR: 'wrr', + SHL: 'wrr', + SHR: 'wrr', + USHR: 'wrr', + LT: 'wrr', + GT: 'wrr', + LTE: 'wrr', + GTE: 'wrr', + EQ: 'wrr', + NEQ: 'wrr', + LOOSE_EQ: 'wrr', + LOOSE_NEQ: 'wrr', + UNARY_NEG: 'wr', + UNARY_POS: 'wr', + UNARY_NOT: 'wr', + UNARY_BITNOT: 'wr', + TYPEOF: 'wr', + MOVE: 'wr', + LOAD_INT: 'wi', + LOAD_CONST: 'wc', + LOAD_GLOBAL: 'wg', + TYPEOF_SAFE: 'wn', + JUMP_IF_FALSE: 'Tl', + JUMP_IF_TRUE: 'Fl', + BUILD_ARRAY: 'wkr', + BUILD_OBJECT: 'wkrr', +}) + +const SAFE_GLOBALS = new Set([ + 'Object', + 'Array', + 'String', + 'Number', + 'Boolean', + 'Math', + 'JSON', + 'Date', + 'RegExp', + 'Error', + 'Function', + 'parseInt', + 'parseFloat', + 'isNaN', + 'isFinite', + 'Infinity', + 'NaN', + 'undefined', +]) +const MAX_FAKE_INT = 65535 +const MAX_MODELS = 4096 +const NAME_PATTERN = /^[A-Za-z0-9]{3,10}$/ +const SEED_STRING_PATTERN = /^[A-Za-z0-9]{3,8}$/ + +class FlowDecline extends Error { + constructor(code, message) { + super(message) + this.code = code + } +} + +function decline(code, message) { + throw new FlowDecline(code, message) +} + +function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) + return value + Object.freeze(value) + for (const child of Object.values(value)) deepFreeze(child) + return value +} + +function constantAt(pool, index, key) { + if (!Number.isSafeInteger(index) || index < 0 || index >= pool.length) + return { valid: false } + try { + return { valid: true, value: restoreConcealedValue(pool[index], key) } + } catch { + return { valid: false } + } +} + +function fakeScalar(value) { + return ( + (Number.isInteger(value) && value >= 0 && value <= MAX_FAKE_INT) || + (typeof value === 'string' && SEED_STRING_PATTERN.test(value)) + ) +} + +function readPair(words, index, pool) { + return constantAt(pool, words[index], words[index + 1]) +} + +function fakeStepMatches( + step, + instruction, + flowing, + anchors, + constants, + fromOrigin, +) { + const layout = FAKE_SLOTS[step.name] + if (!layout) return false + let position = 0 + for (const slot of layout) { + const word = step.words[position] + if (slot === 'w' || slot === 'r') { + if (!flowing.has(word)) return false + position += 1 + } else if (slot === 'T' || slot === 'F') { + if (word !== anchors[slot]) return false + position += 1 + } else if (slot === 'l') { + if (fromOrigin(word) !== instruction.nextPc) return false + position += 1 + } else if (slot === 'i') { + if (word > MAX_FAKE_INT) return false + position += 1 + } else if (slot === 'k') { + if (word > 2) return false + position += 1 + } else { + const constant = readPair(step.words, position, constants) + if (!constant.valid) return false + if (slot === 'c' && !fakeScalar(constant.value)) return false + if (slot === 'g' && !SAFE_GLOBALS.has(constant.value)) return false + if ( + slot === 'n' && + (typeof constant.value !== 'string' || + !NAME_PATTERN.test(constant.value)) + ) + return false + position += 2 + } + } + return position === step.words.length +} + +function realStepMatches(step, fakeRegisters) { + return ( + REAL_ARITIES[step.name] === step.words.length && + step.words.every((word) => !fakeRegisters.has(word)) + ) +} + +function registerReadsOrWrites(instruction) { + const registers = (instruction.wordOperands ?? []) + .filter((operand) => operand.kind === 'register') + .map((operand) => operand.index) + if (instruction.name === 'MAKE_CLOSURE') { + registers.push( + ...instruction.captures + .filter((capture) => capture.isLocal) + .map((capture) => capture.index), + ) + } + return registers +} + +function functionIntervals(instructions, wordCount) { + const boundaries = new Set(instructions.map(({ pc }) => pc)) + const starts = [ + ...new Set([ + 0, + ...instructions + .filter(({ name }) => name === 'MAKE_CLOSURE') + .map(({ functionMeta }) => functionMeta.startPc), + ]), + ].sort((left, right) => left - right) + if (starts.some((pc) => !boundaries.has(pc))) + decline('function-boundary', 'Function entries are not unique boundaries') + return starts.map((startPc, index) => { + const endPc = starts[index + 1] ?? wordCount + const body = instructions.filter(({ pc }) => pc >= startPc && pc < endPc) + if ( + body.length === 0 || + body[0].pc !== startPc || + body.at(-1).nextPc !== endPc + ) + decline('function-boundary', 'Function interval is incomplete') + for (const instruction of body) { + if (instruction.name === 'MAKE_CLOSURE') continue + if ( + instruction.wordOperands?.some( + (operand) => + operand.kind === 'label-target' && + (!boundaries.has(operand.pc) || + operand.pc < startPc || + operand.pc >= endPc), + ) + ) { + decline( + 'function-boundary', + 'Direct target crosses a function interval', + ) + } + } + return { startPc, endPc, body } + }) +} + +// A PATCH suspect is an undecided selfModifying filler: isolation reads past it. Real code never +// touches a seed register, so a suspect that touches one is a decoy (settled here); while the +// optional seed is undecided, a suspect touching only it decides nothing (see recognize). +function seedVariants(interval, constants, fromOrigin, suspects) { + const synthetic = interval.body.filter(({ name }) => name === 'SYNTHETIC') + if (synthetic.length === 0) return [] + const hasJump = synthetic.some(({ steps }) => + steps.some( + ({ name }) => name === 'JUMP_IF_TRUE' || name === 'JUMP_IF_FALSE', + ), + ) + let prefix = 0 + while (interval.body[prefix]?.name === 'LOAD_CONST') prefix += 1 + const variants = [] + for (const count of hasJump ? [4, 5] : [2, 3]) { + if (count > prefix) continue + const seeds = interval.body.slice(0, count).map((instruction) => ({ + register: instruction.words[1], + constant: readPair(instruction.words, 2, constants), + })) + if ( + new Set(seeds.map(({ register }) => register)).size !== count || + seeds.some( + ({ constant }) => !constant.valid || !fakeScalar(constant.value), + ) + ) + continue + const fakeRegisters = new Set(seeds.map(({ register }) => register)) + if ( + interval.body + .slice(count) + .some( + (instruction) => + instruction.name !== 'SYNTHETIC' && + !suspects.has(instruction.pc) && + registerReadsOrWrites(instruction).some((register) => + fakeRegisters.has(register), + ), + ) + ) + continue + const anchorPairs = hasJump + ? seeds.flatMap((truthy) => + seeds + .filter( + (falsy) => + falsy.register !== truthy.register && + typeof truthy.constant.value === 'number' && + truthy.constant.value >= 1 && + falsy.constant.value === 0, + ) + .map((falsy) => ({ T: truthy.register, F: falsy.register })), + ) + : [{ T: null, F: null }] + for (const anchors of anchorPairs) { + const flowing = new Set( + seeds + .map(({ register }) => register) + .filter( + (register) => register !== anchors.T && register !== anchors.F, + ), + ) + const candidates = new Map() + let valid = true + for (const instruction of synthetic) { + const opcode = instruction.opcode.value + const possible = new Set() + for (let index = 0; index < instruction.steps.length; index += 1) { + if (!realStepMatches(instruction.steps[index], fakeRegisters)) + continue + if ( + instruction.steps.every( + (step, position) => + position === index || + fakeStepMatches( + step, + instruction, + flowing, + anchors, + constants, + fromOrigin, + ), + ) + ) + possible.add(index) + } + const previous = candidates.get(opcode) + const narrowed = previous + ? new Set([...previous].filter((index) => possible.has(index))) + : possible + if (narrowed.size === 0) { + valid = false + break + } + candidates.set(opcode, narrowed) + } + if (valid) variants.push({ count, anchors, candidates }) + } + } + if (variants.length === 0) { + decline( + 'register-isolation', + `No fake register model at ${interval.startPc}`, + ) + } + return variants +} + +function intersectCandidates(left, right) { + const merged = new Map(left) + for (const [opcode, positions] of right) { + const prior = merged.get(opcode) + const common = prior + ? new Set([...prior].filter((position) => positions.has(position))) + : positions + if (common.size === 0) return null + merged.set(opcode, common) + } + return merged +} + +function modelKey(model) { + return JSON.stringify( + [...model.entries()] + .sort(([left], [right]) => left - right) + .map(([opcode, positions]) => [opcode, [...positions].sort()]), + ) +} + +// A fake branch targets its own fallthrough. When an earlier stage moved code, that operand is +// still an origin address, and originPcMap (origin PC -> input PC) translates it. +function recognize(container, runtimeObservation, originPcMap, suspects) { + const fromOrigin = (pc) => (originPcMap ? originPcMap.get(pc) : pc) + const scan = diagnoseAntiWordStream(container, runtimeObservation) + if (!scan.ok) decline(scan.diagnostic.code, scan.diagnostic.message) + const constants = container.roles?.pool?.values + if (!Array.isArray(constants)) { + decline('constant-pool', 'the container has no constant pool') + } + const intervals = functionIntervals( + scan.result.instructions, + scan.result.wordCount, + ) + const functions = intervals + .map((interval) => ({ + startPc: interval.startPc, + variants: seedVariants(interval, constants, fromOrigin, suspects), + interval, + })) + .filter(({ variants }) => variants.length > 0) + let solutions = [new Map()] + for (const fn of functions) { + const next = solutions.flatMap((solution) => + fn.variants + .map(({ candidates }) => intersectCandidates(solution, candidates)) + .filter(Boolean), + ) + solutions = [ + ...new Map(next.map((model) => [modelKey(model), model])).values(), + ] + if (solutions.length === 0) { + decline('register-isolation', 'Function models disagree on real steps') + } + if (solutions.length > MAX_MODELS) { + decline('model-limit', 'Synthetic register models exceed the proof limit') + } + } + const realSteps = runtimeObservation.syntheticCases.map((entry) => { + const positions = new Set( + solutions.flatMap((solution) => [...(solution.get(entry.opcode) ?? [])]), + ) + if (positions.size !== 1) { + decline( + 'ambiguous-real-step', + `Opcode ${entry.opcode} has no unique real step`, + ) + } + const index = [...positions][0] + return { opcode: entry.opcode, index, name: entry.steps[index].name } + }) + const selected = new Map( + realSteps.map(({ opcode, index }) => [opcode, index]), + ) + const decoys = new Set() + const seedCountChoices = functions.map(({ startPc, variants, interval }) => { + const counts = [ + ...new Set( + variants + .filter(({ candidates }) => + [...candidates].every(([opcode, positions]) => + positions.has(selected.get(opcode)), + ), + ) + .map(({ count }) => count), + ), + ].sort() + // Seeds below the smallest count are fake in every model; the one above it is fake only in + // the larger model. A suspect touching a fake seed is a decoy. When both counts fit and a + // suspect touches the optional seed, the seed is deferred: kept as code, settled later. + const touching = (register) => + interval.body.filter( + (instruction) => + suspects.has(instruction.pc) && + registerReadsOrWrites(instruction).includes(register), + ) + const count = Math.max(...counts) + let deferredSeed = null + for (const [offset, seed] of interval.body.slice(0, count).entries()) { + const hits = touching(seed.words[1]) + if (hits.length === 0) continue + if (offset >= Math.min(...counts)) { + deferredSeed = seed.pc + continue + } + for (const { pc } of hits) decoys.add(pc) + } + return { startPc, counts, deferredSeed } + }) + return deepFreeze({ + schemaVersion: 'jsconfuser-vm-anti-register-flow-observation.v1', + realSteps, + functionCount: intervals.length, + fusedFunctionCount: functions.length, + seedCountChoices, + decoys: [...decoys].sort((left, right) => left - right), + }) +} + +export function diagnoseAntiRegisterFlow( + container, + runtimeObservation, + { originPcMap = null, suspects = new Set() } = {}, +) { + try { + return deepFreeze({ + ok: true, + result: recognize(container, runtimeObservation, originPcMap, suspects), + diagnostic: null, + }) + } catch (error) { + if (error instanceof FlowDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'anti-register-flow-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} diff --git a/src/vm/jsconfuser-vm/analyze-anti-runtime.js b/src/vm/jsconfuser-vm/analyze-anti-runtime.js new file mode 100644 index 00000000..9bf7ba3d --- /dev/null +++ b/src/vm/jsconfuser-vm/analyze-anti-runtime.js @@ -0,0 +1,93 @@ +import { parse } from '@babel/parser' +import { partitionHandlers } from './locate-handlers.js' +import { requireIdentifiedHandlers } from './identify-handlers.js' +import { + ContainerDecline, + PARSER_OPTIONS, + decline, + deepFreeze, +} from './container-role-helpers.js' + +function recognize(ast, { allowSpecializedTail = false } = {}) { + const view = requireIdentifiedHandlers(ast, 'anti-runtime-container') + const { canonical, extra } = partitionHandlers(view) + if (canonical.length !== Object.keys(view.opcodes).length) { + decline('anti-runtime', 'Canonical opcode cases are incomplete') + } + if (extra.length === 0) { + decline('anti-runtime', 'No synthetic opcode cases were emitted') + } + // The classifier (identify-handlers.js) sorts the added handlers by what they do; a synthetic + // case is three or more canonical steps reading all of its operands up front into one array. + // Its reads are therefore stored positions: step by step, they are the stored position each + // canonical operand takes. + const cases = [] + for (const handler of extra) { + const found = view.added.get(handler.label) + if (found?.kind !== 'anti') continue + if (!handler.ends) + decline('anti-runtime', `Synthetic case ${handler.label} form changed`) + const canonicalToStored = found.steps.flatMap((step) => step.reads) + let operandStart = 0 + const steps = found.steps.map((step) => { + const item = { + name: step.name, + operandStart, + operandCount: step.reads.length, + } + operandStart += step.reads.length + return item + }) + if (steps.length < 3) + decline( + 'anti-runtime-steps', + `Synthetic case ${handler.label} step width changed`, + ) + cases.push({ + opcode: handler.label, + arity: canonicalToStored.length, + canonicalToStored, + steps, + }) + } + if (cases.length === 0) { + decline('anti-runtime', 'No synthetic opcode prefix was emitted') + } + if (!allowSpecializedTail && cases.length !== extra.length) { + decline('anti-runtime', 'Unproved extra opcode cases remain') + } + return deepFreeze({ + schemaVersion: 'jsconfuser-vm-anti-runtime-observation.v1', + syntheticCases: cases, + }) +} + +export function diagnoseAntiRuntime(input, options) { + try { + const ast = typeof input === 'string' ? parse(input, PARSER_OPTIONS) : input + if (ast?.type !== 'File' && ast?.type !== 'Program') { + decline('invalid-input', 'Expected source text or a Babel AST') + } + return deepFreeze({ + ok: true, + result: recognize(ast, options), + diagnostic: null, + }) + } catch (error) { + if (error instanceof ContainerDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'anti-runtime-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} diff --git a/src/vm/jsconfuser-vm/expand-observed-word-stream.js b/src/vm/jsconfuser-vm/expand-observed-word-stream.js new file mode 100644 index 00000000..df7b8879 --- /dev/null +++ b/src/vm/jsconfuser-vm/expand-observed-word-stream.js @@ -0,0 +1,114 @@ +import { selectContinuationLoads } from './finally-continuations.js' + +const DIRECT_TARGET_OFFSETS = Object.freeze({ + JUMP: [1], + JUMP_IF_FALSE: [2], + JUMP_IF_TRUE: [2], + FOR_IN_NEXT: [3], + TRY_SETUP: [1], + FINALLY_SETUP: [1, 4], + MAKE_CLOSURE: [2], +}) + +export class ObservedExpansionDecline extends Error { + constructor(code, message) { + super(message) + this.code = code + } +} + +// Composes an origin map (origin PC -> input PC) with a stage's own map (input PC -> output PC). +export function composePcMaps(originPcMap, pcMap) { + if (!originPcMap) return pcMap + return new Map( + [...originPcMap] + .filter(([, input]) => pcMap.has(input)) + .map(([origin, input]) => [origin, pcMap.get(input)]), + ) +} + +// Words inside an added opcode (macro and specialized steps) still hold origin addresses when an +// earlier stage has moved code; originPcMap maps them to input PCs. Anti's synthetic words are +// copied unchanged: anti reads its own fake operands through the map its caller composes. +export function expandObservedWordStream( + stream, + { includePcMap = false, originPcMap = null } = {}, +) { + const fromOrigin = (target) => { + if (!originPcMap) return target + if (!originPcMap.has(target)) { + throw new ObservedExpansionDecline( + 'origin-target-boundary', + `Target ${target} has no origin instruction boundary`, + ) + } + return originPcMap.get(target) + } + const pcMap = new Map() + const parts = [] + let nextPc = 0 + for (const instruction of stream.instructions) { + pcMap.set(instruction.pc, nextPc) + const steps = + instruction.name === 'MACRO' || instruction.name === 'SPECIALIZED' + ? instruction.steps + : [instruction] + for (const step of steps) { + parts.push({ + originPc: instruction.pc, + pc: nextPc, + name: step.name, + words: step.words.slice(), + encoded: ['MACRO', 'SPECIALIZED'].includes(instruction.name), + }) + nextPc += step.words.length + } + } + pcMap.set(stream.wordCount, nextPc) + const functionStarts = [ + ...new Set([ + 0, + ...parts + .filter(({ name }) => name === 'MAKE_CLOSURE') + .map((part) => + part.encoded ? fromOrigin(part.words[2]) : part.words[2], + ), + ]), + ].sort((left, right) => left - right) + const remap = (target) => { + if (!pcMap.has(target)) { + throw new ObservedExpansionDecline( + 'macro-target-boundary', + `Target ${target} has no original instruction boundary`, + ) + } + return pcMap.get(target) + } + const address = (part, target) => + remap(part.encoded ? fromOrigin(target) : target) + const continuationLoads = selectContinuationLoads( + parts.map((part) => ({ + key: part.pc, + functionId: functionStarts.findLastIndex( + (start) => start <= part.originPc, + ), + name: part.name, + words: part.words, + })), + ) + const words = parts.flatMap((part) => { + const raw = part.words.slice() + for (const offset of DIRECT_TARGET_OFFSETS[part.name] ?? []) { + raw[offset] = address(part, raw[offset]) + } + if (continuationLoads.has(part.pc)) raw[2] = address(part, raw[2]) + return raw + }) + if (words.length !== nextPc) { + throw new ObservedExpansionDecline( + 'macro-expanded-width', + 'Expanded stream did not consume its PC map', + ) + } + return includePcMap ? { words, pcMap, parts } : words +} diff --git a/src/vm/jsconfuser-vm/finally-continuations.js b/src/vm/jsconfuser-vm/finally-continuations.js new file mode 100644 index 00000000..1ec72389 --- /dev/null +++ b/src/vm/jsconfuser-vm/finally-continuations.js @@ -0,0 +1,22 @@ +// The compiler gives each try/finally a dedicated continuation register (a `local::` register, +// never shared) that only label loads write and only the finalizer's JUMP_REG reads. So inside +// one function, every LOAD_INT whose destination is a FINALLY_SETUP continuation register loads +// an address. Items are { key, functionId, name, words }; the result is the set of selected keys. +export function selectContinuationLoads(items) { + const continuations = new Map() + for (const item of items) { + if (item.name !== 'FINALLY_SETUP') continue + if (!continuations.has(item.functionId)) + continuations.set(item.functionId, new Set()) + continuations.get(item.functionId).add(item.words[2]) + } + return new Set( + items + .filter( + (item) => + item.name === 'LOAD_INT' && + continuations.get(item.functionId)?.has(item.words[1]), + ) + .map(({ key }) => key), + ) +} diff --git a/src/vm/jsconfuser-vm/normalize-aliased-wordcode.js b/src/vm/jsconfuser-vm/normalize-aliased-wordcode.js new file mode 100644 index 00000000..8828a51d --- /dev/null +++ b/src/vm/jsconfuser-vm/normalize-aliased-wordcode.js @@ -0,0 +1,227 @@ +import { diagnoseAliasedRuntime } from './observe-aliased-runtime.js' +import { + diagnoseAliasedWordStream, + diagnoseAntiWordStream, + diagnoseSpecializedWordStream, + diagnoseMacroWordStream, + diagnoseWordcode, +} from './read-wordcode.js' +import { diagnoseMacroRuntime } from './observe-macro-runtime.js' +import { diagnoseAntiRuntime } from './analyze-anti-runtime.js' +import { diagnoseSpecializedRuntime } from './observe-specialized-runtime.js' +import { diagnoseNormalizeAntiWordcode } from './normalize-anti-wordcode.js' +import { normalizeExpandedSpecializedDispatcher } from './normalize-specialized-wordcode.js' +import { + ObservedExpansionDecline, + composePcMaps, + expandObservedWordStream, +} from './expand-observed-word-stream.js' + +const DIRECT_TARGET_OFFSETS = Object.freeze({ + JUMP: [1], + JUMP_IF_FALSE: [2], + JUMP_IF_TRUE: [2], + FOR_IN_NEXT: [3], + TRY_SETUP: [1], + FINALLY_SETUP: [1, 4], + MAKE_CLOSURE: [2], +}) + +// An alias keeps its operands in origin space when an earlier stage moved code. Translate the +// projected direct targets, and the continuation literals that stage proved (output PC -> step +// indexes; an alias has one step), through originPcMap. +function translateAliasAddresses( + projected, + originPcMap, + originContinuationLoads, +) { + const fromOrigin = (target) => { + if (!originPcMap.has(target)) { + throw new AliasNormalizationDecline( + 'origin-target-boundary', + `Alias target ${target} has no origin instruction boundary`, + ) + } + return originPcMap.get(target) + } + for (const item of projected.filter(({ alias }) => alias)) { + for (const offset of DIRECT_TARGET_OFFSETS[item.name] ?? []) + item.words[offset] = fromOrigin(item.words[offset]) + if (originContinuationLoads?.get(item.pc)?.includes(0)) + item.words[2] = fromOrigin(item.words[2]) + } +} + +class AliasNormalizationDecline extends Error { + constructor(code, message) { + super(message) + this.code = code + } +} + +function requireObservation(diagnosis) { + if (!diagnosis.ok) { + throw new AliasNormalizationDecline( + diagnosis.diagnostic.code, + diagnosis.diagnostic.message, + ) + } + return diagnosis.result +} + +function recognize( + source, + container, + { + allowObservedPrefix = false, + originPcMap = null, + originContinuationLoads = null, + suspects = [], + } = {}, +) { + const mixed = allowObservedPrefix + const antiDiagnosis = mixed + ? diagnoseAntiRuntime(source, { allowSpecializedTail: true }) + : null + const anti = antiDiagnosis?.ok ? antiDiagnosis.result : null + const specializedDiagnosis = mixed + ? diagnoseSpecializedRuntime(source, { + allowAntiPrefix: Boolean(anti), + allowMacroTail: true, + }) + : null + const specialized = specializedDiagnosis?.ok + ? specializedDiagnosis.result + : null + const macroDiagnosis = mixed + ? diagnoseMacroRuntime(source, { + antiObservation: anti, + specializedObservation: specialized, + allowAliasTail: true, + }) + : null + const macro = macroDiagnosis?.ok ? macroDiagnosis.result : null + if (mixed && !anti && !specialized && !macro) { + throw new AliasNormalizationDecline( + 'alias-prefix-unproved', + 'No earlier opcode prefix was proved', + ) + } + const runtime = requireObservation( + diagnoseAliasedRuntime(source, { + antiObservation: anti, + specializedObservation: specialized, + macroObservation: macro, + }), + ) + const stream = requireObservation( + diagnoseAliasedWordStream(container, runtime, { + antiObservation: anti, + specializedObservation: specialized, + macroObservation: macro, + }), + ) + const byOpcode = new Map( + runtime.aliases.map((entry) => [entry.opcode, entry]), + ) + const projected = stream.instructions.map((instruction) => { + const kept = { pc: instruction.pc, name: instruction.name, alias: false } + if (instruction.name !== 'SYNTHETIC') + return { ...kept, words: instruction.words } + const alias = byOpcode.get(instruction.opcode.value) + if (!alias && anti) return { ...kept, words: instruction.words } + if (!alias) + throw new AliasNormalizationDecline( + 'alias-opcode-mismatch', + 'Alias word stream has no matching runtime case', + ) + return { + pc: instruction.pc, + name: alias.originalName, + alias: true, + words: [alias.originalOpcode, ...instruction.canonicalOperands], + } + }) + if (originPcMap) + translateAliasAddresses(projected, originPcMap, originContinuationLoads) + const words = projected.flatMap(({ words }) => words) + const normalized = structuredClone(container) + normalized.roles.words.values = words + // Projection keeps every width, so without a later expansion the incoming map still holds. + let output = { container: normalized, originPcMap } + if (anti || specialized || macro) { + const projectedStream = requireObservation( + macro + ? diagnoseMacroWordStream(normalized, macro, specialized, anti) + : specialized + ? diagnoseSpecializedWordStream(normalized, specialized, anti) + : diagnoseAntiWordStream(normalized, anti), + ) + const expansion = expandObservedWordStream(projectedStream, { + includePcMap: true, + originPcMap, + }) + const pcMap = composePcMaps(originPcMap, expansion.pcMap) + const expanded = structuredClone(normalized) + expanded.roles.words.values = expansion.words + output = anti + ? requireObservation( + diagnoseNormalizeAntiWordcode(source, expanded, { + allowSpecializedTail: true, + originPcMap: pcMap, + suspects, + }), + ) + : normalizeExpandedSpecializedDispatcher( + source, + expanded, + pcMap, + suspects, + ) + } + if (!output.wordcode) + output = { + ...output, + wordcode: requireObservation(diagnoseWordcode(output.container)), + } + return { + ...output, + schemaVersion: 'jsconfuser-vm-aliased-wordcode-normalization.v1', + originalWordCount: stream.wordCount, + aliasedInstructionCount: stream.instructions.filter( + ({ name, opcode }) => name === 'SYNTHETIC' && byOpcode.has(opcode.value), + ).length, + macroInstructionCount: stream.instructions.filter( + ({ name }) => name === 'MACRO', + ).length, + } +} + +export function diagnoseNormalizeAliasedWordcode(source, container, options) { + try { + return { + ok: true, + result: recognize(source, container, options), + diagnostic: null, + } + } catch (error) { + if ( + error instanceof AliasNormalizationDecline || + error instanceof ObservedExpansionDecline + ) { + return { + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + } + } + return { + ok: false, + result: null, + diagnostic: { + code: 'alias-normalization-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} diff --git a/src/vm/jsconfuser-vm/normalize-anti-wordcode.js b/src/vm/jsconfuser-vm/normalize-anti-wordcode.js new file mode 100644 index 00000000..80db057c --- /dev/null +++ b/src/vm/jsconfuser-vm/normalize-anti-wordcode.js @@ -0,0 +1,367 @@ +import { diagnoseAntiRuntime } from './analyze-anti-runtime.js' +import { diagnoseAntiRegisterFlow } from './analyze-anti-register-flow.js' +import { selectContinuationLoads } from './finally-continuations.js' +import { diagnoseNormalizeConcealedConstants } from './normalize-concealed-constants.js' +import { diagnoseNormalizeDispatcherWordcode } from './normalize-dispatcher-wordcode.js' +import { diagnoseAntiWordStream, diagnoseWordcode } from './read-wordcode.js' +import { validateReferences } from './validate-references.js' +import { diagnoseFunctionPartition } from './partition-functions.js' +import { composePcMaps } from './expand-observed-word-stream.js' + +const DIRECT_TARGET_OFFSETS = Object.freeze({ + JUMP: [1], + JUMP_IF_FALSE: [2], + JUMP_IF_TRUE: [2], + FOR_IN_NEXT: [3], + TRY_SETUP: [1], + FINALLY_SETUP: [1, 4], + MAKE_CLOSURE: [2], +}) + +class NormalizationDecline extends Error { + constructor(code, message) { + super(message) + this.code = code + } +} + +function decline(code, message) { + throw new NormalizationDecline(code, message) +} + +function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) + return value + Object.freeze(value) + for (const child of Object.values(value)) deepFreeze(child) + return value +} + +function requireObservation(diagnosis) { + if (!diagnosis.ok) { + decline(diagnosis.diagnostic.code, diagnosis.diagnostic.message) + } + return diagnosis.result +} + +export function padAntiInstruction(instruction, realStep, opcodes) { + const step = instruction.steps[realStep.index] + const opcode = opcodes[step.name] + const words = [opcode, ...step.words] + let remaining = instruction.width - words.length + if (remaining < 4) { + decline('anti-padding', 'Synthetic instruction has insufficient fake width') + } + const fakeRegister = instruction.steps.find( + (_, index) => index !== realStep.index, + )?.words[0] + if (remaining % 2 === 1) { + words.push(opcodes.MOVE, fakeRegister, fakeRegister) + remaining -= 3 + } + while (remaining > 0) { + words.push(opcodes.LOAD_THIS, fakeRegister) + remaining -= 2 + } + return words +} + +function compactWords( + stream, + flow, + container, + padded, + source, + decodedTargetOriginMap, + suspects, +) { + const decoys = new Set(flow.decoys) + const initialWordcode = requireObservation(diagnoseWordcode(padded)) + const concealed = initialWordcode.instructions.some((instruction) => + instruction.operands.some( + (operand) => operand.kind === 'constant-ref' && operand.concealKey !== 0, + ), + ) + ? requireObservation( + diagnoseNormalizeConcealedConstants(source, padded, initialWordcode), + ) + : { container: padded, wordcode: initialWordcode } + const controlContainer = concealed.container + const paddedWordcode = concealed.wordcode + const references = validateReferences(controlContainer, paddedWordcode) + const functions = + references && + diagnoseFunctionPartition(controlContainer, paddedWordcode, references, { + sharedParentSuspects: true, + }).result + if (!functions) + decline('anti-control-proof', 'Padded stream has no function partition') + const byPc = new Map( + paddedWordcode.instructions.map((item) => [item.pc, item]), + ) + const ownerByPc = new Map( + functions.ownership.map(({ pc, functionId }) => [pc, functionId]), + ) + const labelLoads = selectContinuationLoads( + paddedWordcode.instructions + .filter(({ pc }) => !suspects.has(pc)) + .map((instruction) => ({ + key: instruction.pc, + functionId: ownerByPc.get(instruction.pc), + name: instruction.name, + words: instruction.words, + })), + ) + // A JUMP_REG is a finalizer resume when it reads its function's continuation register and a + // label load of that function writes it; any other JUMP_REG is the dispatcher's. + let requiresDispatcher = false + for (const fn of functions.functions) { + const instructions = fn.instructionPcs.map((pc) => byPc.get(pc)) + const continuations = new Set( + instructions + .filter(({ name }) => name === 'FINALLY_SETUP') + .map(({ words }) => words[2]), + ) + for (const jump of instructions.filter(({ name }) => name === 'JUMP_REG')) { + if ( + !continuations.has(jump.words[1]) || + !instructions.some( + (item) => labelLoads.has(item.pc) && item.words[1] === jump.words[1], + ) + ) + requiresDispatcher = true + } + } + const byOpcode = new Map(flow.realSteps.map((step) => [step.opcode, step])) + const opcodes = container.roles.op.values + const seedPcs = new Set() + const indexByPc = new Map( + stream.instructions.map((instruction, index) => [instruction.pc, index]), + ) + // A deferred seed stays as code: erase only the seeds every model agrees on. + const seedCounts = flow.seedCountChoices.map( + ({ startPc, counts, deferredSeed }) => ({ + startPc, + count: deferredSeed === null ? Math.max(...counts) : Math.min(...counts), + }), + ) + const seedFunctionStarts = new Set(seedCounts.map(({ startPc }) => startPc)) + if (seedCounts.every(({ count }) => Number.isInteger(count))) { + for (const { startPc, count } of seedCounts) { + const index = indexByPc.get(startPc) + for (let offset = 0; offset < count; offset += 1) { + const seed = stream.instructions[index + offset] + if (seed?.name !== 'LOAD_CONST') { + decline('anti-seed-proof', 'Fake seed prefix is not canonical') + } + seedPcs.add(seed.pc) + } + } + for (const instruction of stream.instructions) { + if (instruction.name === 'SYNTHETIC') continue + for (const offset of DIRECT_TARGET_OFFSETS[instruction.name] ?? []) { + const target = instruction.words[offset] + if (seedPcs.has(target) && !seedFunctionStarts.has(target)) { + decline('anti-seed-proof', 'Direct target enters a fake seed prefix') + } + } + } + for (const pc of labelLoads) { + const target = byPc.get(pc).words[2] + if (seedPcs.has(target) && !seedFunctionStarts.has(target)) { + decline('anti-seed-proof', 'Continuation enters a fake seed prefix') + } + } + } + const pcMap = new Map() + let nextPc = 0 + // A decoy suspect is removed with no map entry, so an address naming it declines. + for (const instruction of stream.instructions) { + if (decoys.has(instruction.pc)) continue + pcMap.set(instruction.pc, nextPc) + if (seedPcs.has(instruction.pc)) continue + nextPc += + instruction.name === 'SYNTHETIC' + ? 1 + + instruction.steps[byOpcode.get(instruction.opcode.value).index].words + .length + : instruction.width + } + pcMap.set(stream.wordCount, nextPc) + const remap = (target) => { + if (!pcMap.has(target)) { + decline( + 'anti-control-proof', + `Target ${target} has no compact PC boundary`, + ) + } + return pcMap.get(target) + } + const words = stream.instructions.flatMap((instruction) => { + if (seedPcs.has(instruction.pc) || decoys.has(instruction.pc)) return [] + if (instruction.name === 'SYNTHETIC') { + const real = byOpcode.get(instruction.opcode.value) + return [opcodes[real.name], ...instruction.steps[real.index].words] + } + const raw = instruction.words.slice() + for (const offset of DIRECT_TARGET_OFFSETS[instruction.name] ?? []) { + raw[offset] = remap(raw[offset]) + } + if (labelLoads.has(instruction.pc)) raw[2] = remap(raw[2]) + return raw + }) + const decodedTargetMap = new Map() + const origins = + decodedTargetOriginMap ?? new Map([...pcMap].map(([pc]) => [pc, pc])) + for (const [origin, expanded] of origins) { + if (decoys.has(expanded)) continue + if (!pcMap.has(expanded)) { + decline('anti-control-proof', 'Dispatcher target map lost a boundary') + } + if (seedPcs.has(expanded) && !seedFunctionStarts.has(expanded)) continue + decodedTargetMap.set(origin, pcMap.get(expanded)) + } + return { words, decodedTargetMap, requiresDispatcher } +} + +// originPcMap (origin PC -> input PC) comes from the stages that moved code before this one +// (PATCH removal, macro or specialized expansion), composed by the caller; the fused operands +// stay in origin space, and this stage translates them itself. +function recognize( + source, + container, + { allowSpecializedTail = false, originPcMap = null, suspects = [] } = {}, +) { + const runtime = requireObservation( + diagnoseAntiRuntime(source, { allowSpecializedTail }), + ) + const decodedTargetOriginMap = originPcMap + // PATCH suspects arrive as origin PCs; read them in this input's space. + const toInput = (pc) => + decodedTargetOriginMap ? decodedTargetOriginMap.get(pc) : pc + const inputSuspects = new Set(suspects.map(toInput)) + const stream = requireObservation(diagnoseAntiWordStream(container, runtime)) + const flow = requireObservation( + diagnoseAntiRegisterFlow(container, runtime, { + originPcMap, + suspects: inputSuspects, + }), + ) + const byOpcode = new Map(flow.realSteps.map((step) => [step.opcode, step])) + const opcodes = container.roles.op.values + const normalizedWords = stream.instructions.flatMap((instruction) => + instruction.name === 'SYNTHETIC' + ? padAntiInstruction( + instruction, + byOpcode.get(instruction.opcode.value), + opcodes, + ) + : instruction.words, + ) + const padded = structuredClone(container) + padded.roles.words.values = normalizedWords + let compact = structuredClone(container) + const compaction = compactWords( + stream, + flow, + container, + padded, + source, + decodedTargetOriginMap, + inputSuspects, + ) + compact.roles.words.values = compaction.words + let compactWordcode = requireObservation(diagnoseWordcode(compact)) + let records = {} + if (compaction.requiresDispatcher) { + if ( + compactWordcode.instructions.some((instruction) => + instruction.operands.some( + (operand) => + operand.kind === 'constant-ref' && operand.concealKey !== 0, + ), + ) + ) { + const concealed = requireObservation( + diagnoseNormalizeConcealedConstants(source, compact, compactWordcode), + ) + compact = concealed.container + compactWordcode = concealed.wordcode + } + const references = validateReferences(compact, compactWordcode) + if (!references) + decline('anti-control-proof', 'Compact stream has no references') + const options = { + decodedTargetMap: compaction.decodedTargetMap, + suspects: suspects.filter((pc) => !flow.decoys.includes(toInput(pc))), + } + const functions = requireObservation( + diagnoseFunctionPartition(compact, compactWordcode, references, { + sharedParentSuspects: true, + }), + ) + const dispatcher = requireObservation( + diagnoseNormalizeDispatcherWordcode( + compact, + compactWordcode, + functions, + options, + ), + ) + records = dispatcher + compact = records.container + compactWordcode = records.wordcode + } + // Origin PCs (or the caller's origin space) -> output, through the inner dispatcher when + // it ran. + const { pcMap: dispatcherPcMap, ...inner } = records + return deepFreeze({ + ...inner, + schemaVersion: 'jsconfuser-vm-anti-wordcode-normalization.v1', + container: compact, + wordcode: compactWordcode, + originPcMap: dispatcherPcMap + ? composePcMaps(compaction.decodedTargetMap, dispatcherPcMap) + : compaction.decodedTargetMap, + // Undecided PATCH suspects (decoys settled here leave) and deferred seeds, as origin PCs. + suspects: suspects.filter((pc) => !flow.decoys.includes(toInput(pc))), + deferredSeeds: flow.seedCountChoices + .map(({ deferredSeed }) => deferredSeed) + .filter((pc) => pc !== null) + .map((pc) => + decodedTargetOriginMap + ? [...decodedTargetOriginMap].find(([, input]) => input === pc)[0] + : pc, + ), + originalWordCount: stream.wordCount, + syntheticInstructionCount: stream.instructions.filter( + ({ name }) => name === 'SYNTHETIC', + ).length, + }) +} + +export function diagnoseNormalizeAntiWordcode(source, container, options) { + try { + return deepFreeze({ + ok: true, + result: recognize(source, container, options), + diagnostic: null, + }) + } catch (error) { + if (error instanceof NormalizationDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'anti-normalization-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} diff --git a/src/vm/jsconfuser-vm/normalize-cff-wordcode.js b/src/vm/jsconfuser-vm/normalize-cff-wordcode.js new file mode 100644 index 00000000..4109f3f1 --- /dev/null +++ b/src/vm/jsconfuser-vm/normalize-cff-wordcode.js @@ -0,0 +1,554 @@ +import { selectContinuationLoads } from './finally-continuations.js' +import { diagnoseWordcode } from './read-wordcode.js' +import { diagnoseReferences } from './validate-references.js' +import { diagnoseFunctionPartition } from './partition-functions.js' + +class CffDecline extends Error { + constructor(code, message, refusedBy = null) { + super(message) + this.code = code + this.refusedBy = refusedBy + } +} + +function decline(message) { + throw new CffDecline('cff-shape', message) +} + +// A record builder (readWordcode through partitionFunctions) refusing the rewritten words is that +// builder's refusal, not CFF's. +function requireResult(diagnosis, refusedBy) { + if (!diagnosis.ok) { + throw new CffDecline( + diagnosis.diagnostic.code, + diagnosis.diagnostic.message, + refusedBy, + ) + } + return diagnosis.result +} + +const TARGET_OFFSETS = Object.freeze({ + JUMP: [1], + JUMP_IF_FALSE: [2], + JUMP_IF_TRUE: [2], + FOR_IN_NEXT: [3], + TRY_SETUP: [1], + FINALLY_SETUP: [1, 4], + MAKE_CLOSURE: [2], +}) + +function numericConstant(instruction, pool) { + if ( + instruction?.name !== 'LOAD_CONST' || + instruction.words[3] !== 0 || + typeof pool[instruction.words[2]] !== 'number' || + !Number.isInteger(pool[instruction.words[2]]) || + pool[instruction.words[2]] < 0 || + pool[instruction.words[2]] > 65535 + ) + return null + return pool[instruction.words[2]] +} + +// Reads one function's state loop. The prefix alone is spelling: every non-arrow function +// opens with LOAD_THIS, as the state-loop template does, and a plain loop on two fresh locals +// continues it. A function is CFF only once its whole loop reads (arms, back edge, initial state, +// adjacent exit, body targets); until then any mismatch returns null and the function carries +// through. After that, a refusal is CFF's and declines the whole adapter. +function observeFunction(fn, wordcode, pool) { + const byPc = new Map(wordcode.instructions.map((item) => [item.pc, item])) + const ins = fn.instructionPcs.map((pc) => byPc.get(pc)) + if ( + ins.length < 18 || + ins[0].name !== 'LOAD_THIS' || + ins[1]?.name !== 'LOAD_CONST' || + ins[2]?.name !== 'MOVE' || + ins[3]?.name !== 'LOAD_CONST' || + ins[4]?.name !== 'MOVE' || + ins[5]?.name !== 'LOAD_CONST' || + ins[6]?.name !== 'NEQ' || + ins[7]?.name !== 'JUMP_IF_FALSE' + ) + return null + const startState = numericConstant(ins[1], pool) + const state = ins[2]?.words[1] + if ( + startState === null || + ins[2]?.name !== 'MOVE' || + ins[2].words[2] !== ins[1].words[1] + ) + return null + if ( + numericConstant(ins[3], pool) !== 0 || + ins[4]?.name !== 'MOVE' || + ins[4].words[2] !== ins[3].words[1] + ) + return null + const accumulator = ins[4].words[1] + const loopTop = ins[5].pc + const endState = numericConstant(ins[5], pool) + if ( + endState === null || + ins[6]?.name !== 'NEQ' || + ins[6].words[2] !== state || + ins[6].words[3] !== ins[5].words[1] || + ins[7]?.name !== 'JUMP_IF_FALSE' || + ins[7].words[1] !== ins[6].words[1] + ) + return null + const exitPc = ins[7].words[2] + const stateTargets = new Map([[endState, exitPc]]) + let index = 8 + let previous = null + while (index < ins.length) { + if (ins[index].name === 'JUMP' && ins[index].words[1] === loopTop) { + index += 1 + break + } + const load = ins[index] + let current + if (previous === null) { + current = numericConstant(load, pool) + if ( + current === null || + ins[index + 1]?.name !== 'MOVE' || + ins[index + 1].words[1] !== accumulator || + ins[index + 1].words[2] !== load.words[1] + ) + return null + } else { + const delta = numericConstant(load, pool) + const arithmetic = ins[index + 1] + if ( + delta === null || + !['ADD', 'SUB'].includes(arithmetic?.name) || + arithmetic.words[2] !== accumulator || + arithmetic.words[3] !== load.words[1] || + ins[index + 2]?.name !== 'MOVE' || + ins[index + 2].words[1] !== accumulator || + ins[index + 2].words[2] !== arithmetic.words[1] + ) + return null + current = previous + (arithmetic.name === 'ADD' ? delta : -delta) + } + const offset = previous === null ? 2 : 3 + const compare = ins[index + offset] + const skip = ins[index + offset + 1] + const jump = ins[index + offset + 2] + if ( + compare?.name !== 'EQ' || + compare.words[2] !== state || + compare.words[3] !== accumulator || + skip?.name !== 'JUMP_IF_FALSE' || + skip.words[1] !== compare.words[1] || + skip.words[2] !== jump?.nextPc || + jump?.name !== 'JUMP' || + stateTargets.has(current) + ) + return null + stateTargets.set(current, jump.words[1]) + previous = current + index += offset + 3 + } + if (index < 13 || ins[index - 1]?.name !== 'JUMP') return null + if (!stateTargets.has(startState)) return null + if (ins[index]?.pc !== exitPc) return null + const starts = [...new Set(stateTargets.values())].sort((a, b) => a - b) + if ( + starts[0] !== exitPc || + starts.some((pc) => pc < exitPc || pc >= fn.endPc || !byPc.has(pc)) + ) + return null + const stateByTarget = new Map() + for (const [value, target] of stateTargets) { + if (value !== endState) stateByTarget.set(target, value) + } + const replacements = new Map() + const removed = new Set(ins.slice(0, index).map((item) => item.pc)) + const blockSuccessors = new Map() + const blockHandlers = new Map() + const fakeForms = new Set() + const root = ins[0].pc + replacements.set(root, []) + + function transition(sequence, at, currentState) { + const load = sequence[at] + const arithmetic = sequence[at + 1] + const jump = sequence[at + 2] + if ( + load?.name !== 'LOAD_INT' || + !['ADD', 'SUB'].includes(arithmetic?.name) || + arithmetic.words[1] !== state || + arithmetic.words[2] !== state || + arithmetic.words[3] !== load.words[1] || + jump?.name !== 'JUMP' || + jump.words[1] !== loopTop + ) + return null + const nextState = + currentState + + (arithmetic.name === 'ADD' ? load.words[2] : -load.words[2]) + const target = stateTargets.get(nextState) + if (target === undefined) + decline(`Relative transition reaches unknown state ${nextState}`) + return target + } + + // The encoder's two fake bodies: one transition, or a fork on `state < k` between two. + function fakeForm(segment, currentState) { + if (segment.length === 3) + return transition(segment, 0, currentState) !== null + const [load, less, branch] = segment + const scratch = load?.words[1] + return ( + segment.length === 9 && + load.name === 'LOAD_INT' && + less.name === 'LT' && + less.words[1] === scratch && + less.words[2] === state && + less.words[3] === scratch && + branch.name === 'JUMP_IF_TRUE' && + branch.words[1] === scratch && + branch.words[2] === segment[6].pc && + transition(segment, 3, currentState) !== null && + transition(segment, 6, currentState) !== null + ) + } + + for (let blockIndex = 0; blockIndex < starts.length; blockIndex += 1) { + const start = starts[blockIndex] + const end = starts[blockIndex + 1] ?? fn.endPc + const segment = ins.filter((item) => item.pc >= start && item.pc < end) + const currentState = stateByTarget.get(start) + if (currentState !== undefined && fakeForm(segment, currentState)) + fakeForms.add(start) + const successors = [] + const handlers = [] + let cursor = 0 + if ( + currentState !== undefined && + segment[0]?.name === 'LOAD_INT' && + segment[0].words[1] === state && + segment[0].words[2] === currentState + ) { + removed.add(segment[0].pc) + cursor = 1 + } + while (cursor < segment.length) { + const item = segment[cursor] + const direct = + currentState === undefined + ? null + : transition(segment, cursor, currentState) + if (direct !== null) { + successors.push(direct) + replacements.set(item.pc, [{ name: 'JUMP', words: [null, direct] }]) + removed.add(segment[cursor + 1].pc) + removed.add(segment[cursor + 2].pc) + cursor += 3 + continue + } + if ( + ['JUMP_IF_FALSE', 'JUMP_IF_TRUE'].includes(item.name) && + currentState !== undefined + ) { + const first = transition(segment, cursor + 1, currentState) + const second = transition(segment, cursor + 4, currentState) + if ( + first !== null && + second !== null && + item.words[2] === segment[cursor + 4]?.pc + ) { + successors.push(second, first) + replacements.set(item.pc, [ + { name: item.name, words: [null, item.words[1], second] }, + { name: 'JUMP', words: [null, first] }, + ]) + for (let offset = 1; offset <= 6; offset += 1) + removed.add(segment[cursor + offset].pc) + cursor += 7 + continue + } + } + if (item.name === 'TRY_SETUP' || item.name === 'FINALLY_SETUP') + handlers.push(item.words[1]) + if (item.name === 'FOR_IN_NEXT') handlers.push(item.words[3]) + cursor += 1 + } + blockSuccessors.set(start, successors) + blockHandlers.set(start, handlers) + } + // A fake block has a fresh state no real transition targets, but fakes draw their own targets + // from every state, fakes included. So of the bodies in a fake form, the fakes are the largest + // set that no transition or handler entry outside it targets. Whether code runs never decides: + // a dead source body targets like any other, and everything else stays for the backend. + const entry = stateTargets.get(startState) + const fakeStarts = new Set([...fakeForms].filter((start) => start !== entry)) + for (let changed = true; changed; ) { + changed = false + const targeted = new Set( + starts + .filter((start) => !fakeStarts.has(start)) + .flatMap((start) => [ + ...blockSuccessors.get(start), + ...blockHandlers.get(start), + ]), + ) + for (const start of fakeStarts) + if (targeted.has(start)) { + fakeStarts.delete(start) + changed = true + } + } + const erasedRegisters = new Set( + ins + .filter( + (item) => + (removed.has(item.pc) || replacements.has(item.pc)) && + [ + 'LOAD_THIS', + 'LOAD_CONST', + 'LOAD_INT', + 'MOVE', + 'ADD', + 'SUB', + 'EQ', + 'NEQ', + ].includes(item.name), + ) + .map((item) => item.words[1]), + ) + for (const start of starts) { + // A fake block is removed whole, so its reads of the state register erase nothing. + if (fakeStarts.has(start)) continue + const end = starts[starts.indexOf(start) + 1] ?? fn.endPc + for (const item of ins.filter( + (item) => item.pc >= start && item.pc < end, + )) { + if (replacements.has(item.pc)) { + if ( + replacements + .get(item.pc) + .some( + (replacement) => + ['JUMP_IF_FALSE', 'JUMP_IF_TRUE'].includes(replacement.name) && + erasedRegisters.has(replacement.words[1]), + ) + ) + decline('Condition uses an erased state-loop register') + continue + } + if (removed.has(item.pc)) continue + if ( + item.operands.some( + (operand) => + operand.kind === 'register' && erasedRegisters.has(operand.index), + ) || + item.captures?.some( + (capture) => + capture.kind === 'local' && erasedRegisters.has(capture.index), + ) + ) + decline(`State-loop register escapes into payload at ${item.pc}`) + } + } + // The encoder rewrites a conditional to jump over the transition to its own fallthrough, so a + // rewritten conditional's first successor is the block the compiler placed right after it. A + // fake fork proves nothing, and a block two conditionals claim keeps no proved place. That is + // the only order the stream proves: every other transition stays a jump, and the blocks keep + // the stream's order, so structure is read from the graph, never from the layout. + const claims = new Map() + for (const [start, successors] of blockSuccessors) + if (successors.length === 2 && !fakeStarts.has(start)) + claims.set(successors[0], [...(claims.get(successors[0]) ?? []), start]) + const fallthroughOf = new Map( + [...claims] + .filter(([, owners]) => owners.length === 1) + .map(([fallthrough, [owner]]) => [owner, fallthrough]), + ) + const conditionalOf = new Map( + [...fallthroughOf].map(([owner, fallthrough]) => [fallthrough, owner]), + ) + const blockRanges = starts.map((start, index) => ({ + start, + end: starts[index + 1] ?? fn.endPc, + })) + const orderedStarts = [] + const placed = new Set() + // The root falls into the entry block; each placed block pulls its proved fallthrough after it. + const place = (start) => { + while (start !== undefined && !placed.has(start)) { + placed.add(start) + orderedStarts.push(start) + start = fallthroughOf.get(start) + } + } + place(entry) + for (const start of starts) + if (!conditionalOf.has(start) || start === entry) place(start) + for (const start of starts) place(start) + for (let order = 0; order < orderedStarts.length - 1; order += 1) { + const start = orderedStarts[order] + const next = orderedStarts[order + 1] + if (fallthroughOf.get(start) !== next) continue + const range = blockRanges.find((item) => item.start === start) + const [terminalPc, terminal] = + [...replacements] + .filter(([pc]) => pc >= range.start && pc < range.end) + .at(-1) ?? [] + if (terminal?.length !== 2) continue + const [conditional, jump] = terminal + replacements.set(terminalPc, [ + { + name: + conditional.name === 'JUMP_IF_TRUE' + ? 'JUMP_IF_FALSE' + : 'JUMP_IF_TRUE', + words: [null, conditional.words[1], jump.words[1]], + }, + ]) + } + return { + root, + replacements, + removed, + orderedStarts, + blockRanges, + fakeStarts, + fn, + } +} + +function recognize(container, wordcode, functions) { + const pool = container.roles.pool.values + const plans = functions.functions + .map((fn) => observeFunction(fn, wordcode, pool)) + .filter(Boolean) + if (!plans.length) + throw new CffDecline('cff-absent', 'No pinned flattening state loop found') + const replacements = new Map(plans.flatMap((plan) => [...plan.replacements])) + const removed = new Set(plans.flatMap((plan) => [...plan.removed])) + const planByRoot = new Map(plans.map((plan) => [plan.root, plan])) + const byPc = new Map(wordcode.instructions.map((item) => [item.pc, item])) + const orderedInstructions = [] + for (const fn of functions.functions) { + const plan = planByRoot.get(fn.startPc) + if (!plan) { + for (const pc of fn.instructionPcs) orderedInstructions.push(byPc.get(pc)) + continue + } + orderedInstructions.push(byPc.get(plan.root)) + for (const start of plan.orderedStarts) { + if (plan.fakeStarts.has(start)) continue + const range = plan.blockRanges.find((item) => item.start === start) + for (const pc of fn.instructionPcs) { + if (pc >= range.start && pc < range.end) + orderedInstructions.push(byPc.get(pc)) + } + } + } + const parts = [] + const pcMap = new Map() + let nextPc = 0 + for (const instruction of orderedInstructions) { + const replacement = replacements.get(instruction.pc) + if (replacement) { + pcMap.set(instruction.pc, nextPc) + for (const item of replacement) { + const words = item.words.slice() + words[0] = container.roles.op.values[item.name] + parts.push({ + pc: nextPc, + originPc: instruction.pc, + name: item.name, + words, + }) + nextPc += words.length + } + } else if (removed.has(instruction.pc)) { + pcMap.set(instruction.pc, nextPc) + } else { + pcMap.set(instruction.pc, nextPc) + parts.push({ + pc: nextPc, + originPc: instruction.pc, + name: instruction.name, + words: instruction.words.slice(), + }) + nextPc += instruction.width + } + } + pcMap.set(wordcode.wordCount, nextPc) + const remap = (target) => { + if (!pcMap.has(target)) + decline(`Target ${target} is not an instruction boundary`) + return pcMap.get(target) + } + const continuationLoads = selectContinuationLoads( + parts.map((part) => ({ + key: part.pc, + functionId: functions.functions.findIndex( + (fn) => part.originPc >= fn.startPc && part.originPc < fn.endPc, + ), + name: part.name, + words: part.words, + })), + ) + const words = parts.flatMap((part) => { + const raw = part.words.slice() + for (const offset of TARGET_OFFSETS[part.name] ?? []) + raw[offset] = remap(raw[offset]) + if (continuationLoads.has(part.pc)) raw[2] = remap(raw[2]) + return raw + }) + if (words.length !== nextPc) decline('Normalized stream width changed') + const normalized = structuredClone(container) + normalized.roles.words.values = words + const nextWordcode = requireResult( + diagnoseWordcode(normalized), + 'read-wordcode', + ) + const references = requireResult( + diagnoseReferences(normalized, nextWordcode), + 'validate-references', + ) + const nextFunctions = requireResult( + diagnoseFunctionPartition(normalized, nextWordcode, references, { + sharedParentSuspects: new Set(functions.sharedParentStarts.map(remap)), + }), + 'partition-functions', + ) + return { + schemaVersion: 'jsconfuser-vm-cff-normalization.v1', + container: normalized, + wordcode: nextWordcode, + references, + functions: nextFunctions, + pcMap, + functionCount: plans.length, + originalWordCount: wordcode.wordCount, + normalizedWordCount: words.length, + } +} + +export function diagnoseNormalizeCffWordcode(container, wordcode, functions) { + try { + return { + ok: true, + result: recognize(container, wordcode, functions), + diagnostic: null, + } + } catch (error) { + return { + ok: false, + result: null, + diagnostic: { + code: + error instanceof CffDecline ? error.code : 'cff-normalization-error', + message: error instanceof Error ? error.message : String(error), + refusedBy: error instanceof CffDecline ? error.refusedBy : null, + }, + } + } +} diff --git a/src/vm/jsconfuser-vm/normalize-concealed-constants.js b/src/vm/jsconfuser-vm/normalize-concealed-constants.js new file mode 100644 index 00000000..5d743582 --- /dev/null +++ b/src/vm/jsconfuser-vm/normalize-concealed-constants.js @@ -0,0 +1,111 @@ +import { diagnoseWordcode } from './read-wordcode.js' + +class ConcealedDecline extends Error { + constructor(code, message) { + super(message) + this.code = code + } +} + +function decline(code, message) { + throw new ConcealedDecline(code, message) +} + +function deconcealString(value, key) { + if ( + !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test( + value, + ) + ) { + decline('concealed-string', 'Concealed string is not canonical base64') + } + const bytes = Buffer.from(value, 'base64') + if (bytes.toString('base64') !== value || bytes.length % 2 !== 0) { + decline('concealed-string', 'Concealed string is not u16 LE base64') + } + let state = key + let result = '' + for (let index = 0; index < bytes.length; index += 2) { + state = (state + 0x9e3779b9) | 0 + const word = (state ^ (state >>> 13)) & 0xffff + result += String.fromCharCode( + (bytes[index] | (bytes[index + 1] << 8)) ^ word, + ) + } + return result +} + +export function restoreConcealedValue(value, key) { + if (key === 0) return value + if (typeof value === 'number' && Number.isInteger(value)) return value ^ key + if (typeof value === 'string') return deconcealString(value, key) + decline('concealed-value', 'Pool entry has no concealable value') +} + +function recognize(container, wordcode) { + const pairs = [] + const keys = new Map() + for (const instruction of wordcode.instructions) { + const offset = instruction.name === 'STORE_GLOBAL' ? 2 : 3 + for (const operand of instruction.operands) { + if (operand.kind !== 'constant-ref') continue + const { index, concealKey: key } = operand + if (keys.has(index) && keys.get(index) !== key) { + decline('concealed-key', `Pool index ${index} has inconsistent keys`) + } + keys.set(index, key) + pairs.push({ index, key, keyPc: instruction.pc + offset }) + } + } + if (!pairs.some(({ key }) => key !== 0)) { + decline('no-concealed-constants', 'No concealed constant operand exists') + } + const normalized = structuredClone(container) + const pool = normalized.roles.pool.values + for (const [index, key] of keys) { + if (index >= pool.length) { + decline('concealed-index', `Pool index ${index} is out of range`) + } + if (key === 0) continue + pool[index] = restoreConcealedValue(pool[index], key) + } + for (const { key, keyPc } of pairs) { + if (key !== 0) normalized.roles.words.values[keyPc] = 0 + } + const canonical = diagnoseWordcode(normalized) + if (!canonical.ok) { + decline('concealed-wordcode', canonical.diagnostic.message) + } + return { + schemaVersion: 'jsconfuser-vm-concealed-constants-normalization.v1', + container: normalized, + wordcode: canonical.result, + concealedPoolEntries: [...keys.values()].filter((key) => key !== 0).length, + } +} + +export function diagnoseNormalizeConcealedConstants( + source, + container, + wordcode, +) { + try { + return { + ok: true, + result: recognize(container, wordcode), + diagnostic: null, + } + } catch (error) { + return { + ok: false, + result: null, + diagnostic: { + code: + error instanceof ConcealedDecline + ? error.code + : 'concealed-normalization-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} diff --git a/src/vm/jsconfuser-vm/normalize-dispatcher-wordcode.js b/src/vm/jsconfuser-vm/normalize-dispatcher-wordcode.js new file mode 100644 index 00000000..aec25515 --- /dev/null +++ b/src/vm/jsconfuser-vm/normalize-dispatcher-wordcode.js @@ -0,0 +1,205 @@ +import { selectContinuationLoads } from './finally-continuations.js' +import { + diagnoseDispatcherWordcode, + DispatcherDecline, +} from './observe-dispatcher-wordcode.js' +import { diagnoseWordcode } from './read-wordcode.js' +import { diagnoseReferences } from './validate-references.js' +import { diagnoseFunctionPartition } from './partition-functions.js' + +const DIRECT_TARGET_OFFSETS = Object.freeze({ + JUMP: [1], + JUMP_IF_FALSE: [2], + JUMP_IF_TRUE: [2], + FOR_IN_NEXT: [3], + TRY_SETUP: [1], + FINALLY_SETUP: [1, 4], + MAKE_CLOSURE: [2], +}) + +function decline(code, message) { + throw new DispatcherDecline(code, message) +} + +function requireObservation(diagnosis) { + if (!diagnosis.ok) { + decline(diagnosis.diagnostic.code, diagnosis.diagnostic.message) + } + return diagnosis.result +} + +function recognize(container, wordcode, functions, partitionOptions) { + const observation = requireObservation( + diagnoseDispatcherWordcode( + container, + wordcode, + functions, + partitionOptions?.decodedTargetMap, + partitionOptions?.suspects, + ), + ) + const routeByStart = new Map() + const consumed = new Set() + const removed = new Set() + for (const plan of observation.plans) { + removed.add(plan.hoistPc) + for (const pc of plan.dispatcherPcs) removed.add(pc) + const closure = functions.functions.find( + (item) => item.id === plan.closureId, + ) + if (!closure) + decline('dispatcher-closure', 'Decode closure owner is missing') + for (const pc of closure.instructionPcs) removed.add(pc) + for (const route of plan.routes) { + if (routeByStart.has(route.startPc)) { + decline('dispatcher-shape', 'Dispatcher routes share a start PC') + } + routeByStart.set(route.startPc, route) + for (const pc of route.oldPcs) consumed.add(pc) + } + } + for (const pc of consumed) { + if (removed.has(pc)) + decline('dispatcher-shape', 'A route overlaps removed code') + } + + const opcodes = container.roles.op.values + const pcMap = new Map() + const parts = [] + let nextPc = 0 + for (const instruction of wordcode.instructions) { + const route = routeByStart.get(instruction.pc) + if (route) { + pcMap.set(instruction.pc, nextPc) + const words = + route.name === 'JUMP' + ? [opcodes.JUMP, route.target] + : [opcodes[route.name], route.condition, route.target] + parts.push({ + originPc: instruction.pc, + pc: nextPc, + name: route.name, + words, + }) + nextPc += words.length + } else if (consumed.has(instruction.pc)) { + continue + } else if (removed.has(instruction.pc)) { + if (observation.plans.some((plan) => plan.hoistPc === instruction.pc)) { + pcMap.set(instruction.pc, nextPc) + } + continue + } else { + pcMap.set(instruction.pc, nextPc) + parts.push({ + originPc: instruction.pc, + pc: nextPc, + name: instruction.name, + words: instruction.words.slice(), + }) + nextPc += instruction.width + } + } + pcMap.set(wordcode.wordCount, nextPc) + const remap = (target) => { + if (!pcMap.has(target)) { + decline( + 'dispatcher-target-boundary', + `Target ${target} is removed by normalization`, + ) + } + return pcMap.get(target) + } + // A PATCH suspect is read past: a suspect load keeps its words until its owner settles it. + const decodedTargetMap = partitionOptions?.decodedTargetMap + const suspects = new Set( + (partitionOptions?.suspects ?? []).map((pc) => + decodedTargetMap ? decodedTargetMap.get(pc) : pc, + ), + ) + const continuationLoads = selectContinuationLoads( + parts + .filter(({ originPc }) => !suspects.has(originPc)) + .map((part) => ({ + key: part.pc, + functionId: functions.functions.findIndex( + (fn) => part.originPc >= fn.startPc && part.originPc < fn.endPc, + ), + name: part.name, + words: part.words, + })), + ) + const words = parts.flatMap((part) => { + const raw = part.words.slice() + for (const offset of DIRECT_TARGET_OFFSETS[part.name] ?? []) { + raw[offset] = remap(raw[offset]) + } + if (continuationLoads.has(part.pc)) raw[2] = remap(raw[2]) + return raw + }) + if (words.length !== nextPc) { + decline( + 'dispatcher-width', + 'Normalized word stream did not consume its PC map', + ) + } + const normalized = structuredClone(container) + normalized.roles.words.values = words + const newWordcode = requireObservation(diagnoseWordcode(normalized)) + const references = requireObservation( + diagnoseReferences(normalized, newWordcode), + ) + const newFunctions = requireObservation( + diagnoseFunctionPartition(normalized, newWordcode, references, { + sharedParentSuspects: new Set(functions.sharedParentStarts.map(remap)), + }), + ) + return { + schemaVersion: 'jsconfuser-vm-dispatcher-normalization.v1', + container: normalized, + wordcode: newWordcode, + references, + functions: newFunctions, + pcMap, + originalWordCount: wordcode.wordCount, + normalizedWordCount: words.length, + routeCount: observation.plans.reduce( + (count, plan) => count + plan.routes.length, + 0, + ), + decoderClosureCount: observation.plans.length, + // Labels the rewritten conditional routes reach by fallthrough, as the encoder wrote them. + skipLabels: observation.plans.flatMap((plan) => + plan.routes + .filter(({ skipOrigin }) => skipOrigin !== undefined) + .map(({ skipOrigin }) => skipOrigin), + ), + } +} + +export function diagnoseNormalizeDispatcherWordcode( + container, + wordcode, + functions, + partitionOptions, +) { + try { + return { + ok: true, + result: recognize(container, wordcode, functions, partitionOptions), + diagnostic: null, + } + } catch (error) { + return { + ok: false, + result: null, + diagnostic: { + code: + error instanceof DispatcherDecline + ? error.code + : 'dispatcher-normalization-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} diff --git a/src/vm/jsconfuser-vm/normalize-macro-wordcode.js b/src/vm/jsconfuser-vm/normalize-macro-wordcode.js new file mode 100644 index 00000000..2350dd37 --- /dev/null +++ b/src/vm/jsconfuser-vm/normalize-macro-wordcode.js @@ -0,0 +1,107 @@ +import { diagnoseMacroRuntime } from './observe-macro-runtime.js' +import { diagnoseAntiRuntime } from './analyze-anti-runtime.js' +import { diagnoseSpecializedRuntime } from './observe-specialized-runtime.js' +import { diagnoseNormalizeAntiWordcode } from './normalize-anti-wordcode.js' +import { normalizeExpandedSpecializedDispatcher } from './normalize-specialized-wordcode.js' +import { + ObservedExpansionDecline, + composePcMaps, + expandObservedWordStream, +} from './expand-observed-word-stream.js' +import { diagnoseMacroWordStream } from './read-wordcode.js' + +function requireObservation(diagnosis) { + if (!diagnosis.ok) { + throw new ObservedExpansionDecline( + diagnosis.diagnostic.code, + diagnosis.diagnostic.message, + ) + } + return diagnosis.result +} + +function recognize( + source, + container, + { originPcMap = null, suspects = [] } = {}, +) { + const antiDiagnosis = diagnoseAntiRuntime(source, { + allowSpecializedTail: true, + }) + const anti = antiDiagnosis.ok ? antiDiagnosis.result : null + const specializedDiagnosis = diagnoseSpecializedRuntime(source, { + allowAntiPrefix: Boolean(anti), + allowMacroTail: true, + }) + const specialized = specializedDiagnosis.ok + ? specializedDiagnosis.result + : null + const runtime = requireObservation( + diagnoseMacroRuntime(source, { + antiObservation: anti, + specializedObservation: specialized, + }), + ) + const stream = requireObservation( + diagnoseMacroWordStream(container, runtime, specialized, anti), + ) + const normalized = structuredClone(container) + const expansion = expandObservedWordStream(stream, { + includePcMap: true, + originPcMap, + }) + const pcMap = composePcMaps(originPcMap, expansion.pcMap) + normalized.roles.words.values = expansion.words + let output + if (anti) { + output = requireObservation( + diagnoseNormalizeAntiWordcode(source, normalized, { + allowSpecializedTail: true, + originPcMap: pcMap, + suspects, + }), + ) + } else { + output = normalizeExpandedSpecializedDispatcher( + source, + normalized, + pcMap, + suspects, + ) + } + return { + ...output, + schemaVersion: 'jsconfuser-vm-macro-wordcode-normalization.v1', + originalWordCount: stream.wordCount, + expandedWordCount: normalized.roles.words.values.length, + macroInstructionCount: stream.instructions.filter( + ({ name }) => name === 'MACRO', + ).length, + } +} + +export function diagnoseNormalizeMacroWordcode(source, container, options) { + try { + return { + ok: true, + result: recognize(source, container, options), + diagnostic: null, + } + } catch (error) { + if (error instanceof ObservedExpansionDecline) { + return { + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + } + } + return { + ok: false, + result: null, + diagnostic: { + code: 'macro-normalization-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} diff --git a/src/vm/jsconfuser-vm/normalize-self-modifying-wordcode.js b/src/vm/jsconfuser-vm/normalize-self-modifying-wordcode.js new file mode 100644 index 00000000..1064d793 --- /dev/null +++ b/src/vm/jsconfuser-vm/normalize-self-modifying-wordcode.js @@ -0,0 +1,339 @@ +import { diagnoseSelfModifyingRegions } from './observe-self-modifying.js' +import { diagnoseWordcode } from './read-wordcode.js' +import { diagnosePatchAddedOpcodes } from './observe-patch-added-opcodes.js' +import { selectContinuationLoads } from './finally-continuations.js' + +const DIRECT_TARGET_OFFSETS = Object.freeze({ + JUMP: [1], + JUMP_IF_FALSE: [2], + JUMP_IF_TRUE: [2], + FOR_IN_NEXT: [3], + TRY_SETUP: [1], + FINALLY_SETUP: [1, 4], + MAKE_CLOSURE: [2], +}) +const ADDED = new Set(['ALIAS', 'MACRO', 'SPECIALIZED', 'SYNTHETIC']) + +class NormalizationDecline extends Error { + constructor(code, message) { + super(message) + this.code = code + } +} + +function decline(code, message) { + throw new NormalizationDecline(code, message) +} + +function stepsOf(instruction) { + return instruction.steps && instruction.name !== 'SYNTHETIC' + ? instruction.steps + : [instruction] +} + +// Removes what selfModifying added (PATCH headers, decoy PATCHes, decoy fillers and the storage +// after the code) and keeps every other word as the encoder left it. Only canonical instructions' +// addresses are relocated here; an added opcode keeps origin addresses, which its own stage +// translates through the published originPcMap (origin PC -> output PC) and +// originContinuationLoads. A suspect filler stays as code with a map entry, published by its origin +// PC; it is read past here, so a suspect LOAD_INT is never a continuation load and keeps its words. +function compact(container, observation) { + const words = observation.patchedWords + const pcMap = new Map() + const kept = [] + let nextPc = 0 + const fillers = new Set(observation.fillers) + const suspects = new Set(observation.suspects) + // A block's label can sit on its PATCH header, which forwards to the block's first decrypted + // instruction. Decoys and fillers sit after a terminator and before the next label, so no + // address names one; they get no entry and a target naming one declines. + for (const instruction of observation.instructions) { + if (instruction.name === 'PATCH') pcMap.set(instruction.pc, nextPc) + if ( + instruction.name === 'PATCH' || + instruction.name === 'DECOY' || + fillers.has(instruction.pc) + ) + continue + pcMap.set(instruction.pc, nextPc) + kept.push(instruction) + nextPc += instruction.nextPc - instruction.pc + } + pcMap.set(observation.end, nextPc) + const remap = (target) => { + if (!pcMap.has(target)) { + decline('self-modifying-target', `PC ${target} is not recovered code`) + } + return pcMap.get(target) + } + const functionStarts = [ + ...new Set([ + 0, + ...kept.flatMap((instruction) => + stepsOf(instruction) + .filter(({ name }) => name === 'MAKE_CLOSURE') + .map(({ words: stepWords }) => stepWords[2]), + ), + ]), + ].sort((left, right) => left - right) + const functionId = (pc) => + functionStarts.findLastIndex((start) => start <= pc) + const selected = selectContinuationLoads( + kept + .filter((instruction) => !suspects.has(instruction.pc)) + .flatMap((instruction) => + stepsOf(instruction).map((step, index) => ({ + key: { instruction, index }, + functionId: functionId(instruction.pc), + name: step.name, + words: step.words, + })), + ), + ) + const continuationLoads = new Set() + // Continuation loads inside added opcodes keep their origin values for the opcode's own stage; + // publish them (output PC -> step indexes) so that stage translates this proof rather than + // selecting again over a view where another added opcode can hide the FINALLY_SETUP. + const originContinuationLoads = new Map() + for (const { instruction, index } of selected) { + if (!ADDED.has(instruction.name)) { + continuationLoads.add(instruction.pc) + continue + } + const pc = pcMap.get(instruction.pc) + if (!originContinuationLoads.has(pc)) originContinuationLoads.set(pc, []) + originContinuationLoads.get(pc).push(index) + } + const compactWords = kept.flatMap((instruction) => { + const raw = words.slice(instruction.pc, instruction.nextPc) + if (ADDED.has(instruction.name)) return raw + for (const offset of DIRECT_TARGET_OFFSETS[instruction.name] ?? []) + raw[offset] = remap(raw[offset]) + if (continuationLoads.has(instruction.pc)) raw[2] = remap(raw[2]) + return raw + }) + const normalized = structuredClone(container) + normalized.roles.words.values = compactWords + return { container: normalized, pcMap, kept, originContinuationLoads } +} + +export function diagnoseNormalizeSelfModifyingWordcode( + container, + { source = null } = {}, +) { + try { + const runtime = source + ? diagnosePatchAddedOpcodes(source) + : { ok: true, result: {} } + if (!runtime.ok) + decline(runtime.diagnostic.code, runtime.diagnostic.message) + const observed = diagnoseSelfModifyingRegions(container, runtime.result) + if (!observed.ok) { + decline(observed.diagnostic.code, observed.diagnostic.message) + } + const compacted = compact(container, observed.result) + const wordcode = diagnoseWordcode(compacted.container) + return { + ok: true, + result: { + schemaVersion: 'jsconfuser-vm-self-modifying-normalization.v2', + container: compacted.container, + wordcode: wordcode.ok ? wordcode.result : null, + originPcMap: compacted.pcMap, + originContinuationLoads: compacted.originContinuationLoads, + suspects: observed.result.suspects, + suspectRuns: observed.result.suspectRuns, + originalWordCount: container.roles.words.values.length, + wordCount: compacted.container.roles.words.values.length, + patchCount: observed.result.patches.length, + decoyCount: observed.result.decoys.length, + fillerCount: observed.result.fillers.length, + addedOpcodeCounts: Object.fromEntries( + [...ADDED].map((name) => [ + name, + compacted.kept.filter((instruction) => instruction.name === name) + .length, + ]), + ), + }, + diagnostic: null, + } + } catch (error) { + return { + ok: false, + result: null, + diagnostic: { + code: + error instanceof NormalizationDecline + ? error.code + : 'normalizer-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} + +// Settles PATCH suspects once a later decoder has recovered the labels they hid: the filler rule +// reruns over the current canonical stream. Labels are complete when every JUMP_REG is a +// finalizer's (it reads its FINALLY_SETUP continuation register); otherwise nothing is settled. In +// each run selfModifying planted after a terminator, the entries before the first one a label names +// are fillers and leave; that entry and the rest are code. A suspect that is code and loads a +// continuation still holds an origin address, resolved here through originPcMap. Returns null when +// nothing can be settled yet; suspects is then unchanged. +export function diagnoseSettleSelfModifyingSuspects( + container, + wordcode, + { suspects, suspectRuns, originPcMap, recoveredLabels = [] }, +) { + try { + const current = new Map( + suspects + .filter((pc) => originPcMap.has(pc)) + .map((pc) => [originPcMap.get(pc), pc]), + ) + const instructions = wordcode.instructions + const functionStarts = [ + ...new Set([ + 0, + ...instructions + .filter(({ name }) => name === 'MAKE_CLOSURE') + .map(({ words }) => words[2]), + ]), + ].sort((left, right) => left - right) + const functionId = (pc) => + functionStarts.findLastIndex((start) => start <= pc) + const continuations = new Set( + instructions + .filter(({ name }) => name === 'FINALLY_SETUP') + .map(({ pc, words }) => `${functionId(pc)}:${words[2]}`), + ) + if ( + instructions.some( + ({ name, pc, words }) => + name === 'JUMP_REG' && + !continuations.has(`${functionId(pc)}:${words[1]}`), + ) + ) + return { ok: true, result: null, diagnostic: null } + // Labels a later stage recovered that the stream reaches only by fallthrough (dispatcher skip + // labels), as origin PCs. + const labels = new Set([ + ...functionStarts, + ...recoveredLabels.map((pc) => originPcMap.get(pc)), + ]) + for (const instruction of instructions) { + if (current.has(instruction.pc)) continue + for (const offset of DIRECT_TARGET_OFFSETS[instruction.name] ?? []) + labels.add(instruction.words[offset]) + } + const items = (keep) => + instructions + .filter(({ pc }) => keep(pc)) + .map((instruction) => ({ + key: instruction, + functionId: functionId(instruction.pc), + name: instruction.name, + words: instruction.words, + })) + for (const key of selectContinuationLoads(items((pc) => !current.has(pc)))) + labels.add(key.words[2]) + const runs = suspectRuns + .map((run) => + run + .filter((pc) => current.has(originPcMap.get(pc))) + .map((pc) => originPcMap.get(pc)), + ) + .filter((run) => run.length > 0) + const indexByPc = new Map(instructions.map(({ pc }, index) => [pc, index])) + // A code suspect that jumps or loads a continuation names a label (a dispatcher route can start + // on a suspect); repeat until no run changes. + let code = new Set() + let addresses = new Set() + for (;;) { + const next = new Set() + for (const run of runs) { + const first = run.findIndex((pc) => labels.has(pc)) + if (first >= 0) for (const pc of run.slice(first)) next.add(pc) + } + addresses = selectContinuationLoads( + items((pc) => !current.has(pc) || next.has(pc)), + ) + let grew = false + for (const pc of next) { + const instruction = instructions[indexByPc.get(pc)] + for (const offset of DIRECT_TARGET_OFFSETS[instruction.name] ?? []) { + if (labels.has(instruction.words[offset])) continue + labels.add(instruction.words[offset]) + grew = true + } + } + for (const key of addresses) { + if (!next.has(key.pc)) continue + const target = originPcMap.get(key.words[2]) + if (target === undefined) + decline('self-modifying-target', `PC ${key.words[2]} is not code`) + if (!labels.has(target)) { + labels.add(target) + grew = true + } + } + code = next + if (!grew) break + } + const fillers = new Set([...current.keys()].filter((pc) => !code.has(pc))) + const pcMap = new Map() + let nextPc = 0 + for (const instruction of instructions) { + if (fillers.has(instruction.pc)) continue + pcMap.set(instruction.pc, nextPc) + nextPc += instruction.nextPc - instruction.pc + } + pcMap.set(wordcode.wordCount, nextPc) + const remap = (target) => { + if (!pcMap.has(target)) + decline('self-modifying-target', `PC ${target} is not recovered code`) + return pcMap.get(target) + } + const loads = new Set([...addresses].map(({ pc }) => pc)) + const words = instructions.flatMap((instruction) => { + if (fillers.has(instruction.pc)) return [] + const raw = instruction.words.slice() + for (const offset of DIRECT_TARGET_OFFSETS[instruction.name] ?? []) + raw[offset] = remap(raw[offset]) + if (loads.has(instruction.pc)) + raw[2] = remap( + code.has(instruction.pc) ? originPcMap.get(raw[2]) : raw[2], + ) + return raw + }) + const normalized = structuredClone(container) + normalized.roles.words.values = words + const settled = diagnoseWordcode(normalized) + if (!settled.ok) + decline(settled.diagnostic.code, settled.diagnostic.message) + return { + ok: true, + result: { + container: normalized, + wordcode: settled.result, + pcMap, + suspects: [], + suspectRuns: [], + fillerCount: fillers.size, + }, + diagnostic: null, + } + } catch (error) { + return { + ok: false, + result: null, + diagnostic: { + code: + error instanceof NormalizationDecline + ? error.code + : 'normalizer-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} diff --git a/src/vm/jsconfuser-vm/normalize-specialized-wordcode.js b/src/vm/jsconfuser-vm/normalize-specialized-wordcode.js new file mode 100644 index 00000000..0669f8b3 --- /dev/null +++ b/src/vm/jsconfuser-vm/normalize-specialized-wordcode.js @@ -0,0 +1,216 @@ +import { diagnoseSpecializedRuntime } from './observe-specialized-runtime.js' +import { diagnoseAntiRuntime } from './analyze-anti-runtime.js' +import { diagnoseNormalizeAntiWordcode } from './normalize-anti-wordcode.js' +import { diagnoseNormalizeConcealedConstants } from './normalize-concealed-constants.js' +import { diagnoseNormalizeDispatcherWordcode } from './normalize-dispatcher-wordcode.js' +import { diagnoseFunctionPartition } from './partition-functions.js' +import { validateReferences } from './validate-references.js' +import { + diagnoseAntiWordStream, + diagnoseSpecializedWordStream, + diagnoseWordcode, +} from './read-wordcode.js' +import { + ObservedExpansionDecline, + composePcMaps, + expandObservedWordStream, +} from './expand-observed-word-stream.js' + +function requireObservation(diagnosis) { + if (!diagnosis.ok) { + throw new ObservedExpansionDecline( + diagnosis.diagnostic.code, + diagnosis.diagnostic.message, + ) + } + return diagnosis.result +} + +export function normalizeExpandedSpecializedDispatcher( + source, + normalized, + pcMap, + suspects = [], +) { + let wordcode = requireObservation(diagnoseWordcode(normalized)) + let output = { container: normalized, wordcode, originPcMap: pcMap } + if (wordcode.instructions.some(({ name }) => name === 'JUMP_REG')) { + let prepared = normalized + if ( + wordcode.instructions.some((instruction) => + instruction.operands.some( + (operand) => + operand.kind === 'constant-ref' && operand.concealKey !== 0, + ), + ) + ) { + const concealed = requireObservation( + diagnoseNormalizeConcealedConstants(source, prepared, wordcode), + ) + prepared = concealed.container + wordcode = concealed.wordcode + } + const references = validateReferences(prepared, wordcode) + if (!references) { + throw new ObservedExpansionDecline( + 'specialized-references', + 'Expanded stream has no valid references', + ) + } + const options = { + decodedTargetMap: pcMap, + suspects, + } + const functions = requireObservation( + diagnoseFunctionPartition(prepared, wordcode, references, { + sharedParentSuspects: true, + }), + ) + const dispatcher = diagnoseNormalizeDispatcherWordcode( + prepared, + wordcode, + functions, + options, + ) + if (dispatcher.ok) { + const { pcMap: dispatcherPcMap, ...result } = dispatcher.result + output = { + ...result, + originPcMap: composePcMaps(pcMap, dispatcherPcMap), + } + } else if (dispatcher.diagnostic.code !== 'no-dispatcher') + requireObservation(dispatcher) + } + return output +} + +function recognize( + source, + container, + { originPcMap = null, suspects = [] } = {}, +) { + const runtime = requireObservation(diagnoseSpecializedRuntime(source)) + const stream = requireObservation( + diagnoseSpecializedWordStream(container, runtime), + ) + const normalized = structuredClone(container) + const expansion = expandObservedWordStream(stream, { + includePcMap: true, + originPcMap, + }) + normalized.roles.words.values = expansion.words + const output = normalizeExpandedSpecializedDispatcher( + source, + normalized, + composePcMaps(originPcMap, expansion.pcMap), + suspects, + ) + return { + ...output, + schemaVersion: 'jsconfuser-vm-specialized-wordcode-normalization.v1', + originalWordCount: stream.wordCount, + expandedWordCount: normalized.roles.words.values.length, + specializedInstructionCount: stream.instructions.filter( + ({ name }) => name === 'SPECIALIZED', + ).length, + } +} + +function recognizeAntiSpecialized( + source, + container, + { originPcMap = null, suspects = [] } = {}, +) { + const anti = requireObservation( + diagnoseAntiRuntime(source, { allowSpecializedTail: true }), + ) + const specialized = requireObservation( + diagnoseSpecializedRuntime(source, { allowAntiPrefix: true }), + ) + const stream = requireObservation( + diagnoseSpecializedWordStream(container, specialized, anti), + ) + const expanded = structuredClone(container) + const expansion = expandObservedWordStream(stream, { + includePcMap: true, + originPcMap, + }) + expanded.roles.words.values = expansion.words + requireObservation(diagnoseAntiWordStream(expanded, anti)) + const normalized = requireObservation( + diagnoseNormalizeAntiWordcode(source, expanded, { + allowSpecializedTail: true, + originPcMap: composePcMaps(originPcMap, expansion.pcMap), + suspects, + }), + ) + return { + ...normalized, + schemaVersion: 'jsconfuser-vm-anti-specialized-wordcode-normalization.v1', + originalWordCount: stream.wordCount, + specializedInstructionCount: stream.instructions.filter( + ({ name }) => name === 'SPECIALIZED', + ).length, + syntheticInstructionCount: normalized.syntheticInstructionCount, + } +} + +export function diagnoseNormalizeAntiSpecializedWordcode( + source, + container, + options, +) { + try { + return { + ok: true, + result: recognizeAntiSpecialized(source, container, options), + diagnostic: null, + } + } catch (error) { + if (error instanceof ObservedExpansionDecline) { + return { + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + } + } + return { + ok: false, + result: null, + diagnostic: { + code: 'anti-specialized-normalization-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} + +export function diagnoseNormalizeSpecializedWordcode( + source, + container, + options, +) { + try { + return { + ok: true, + result: recognize(source, container, options), + diagnostic: null, + } + } catch (error) { + if (error instanceof ObservedExpansionDecline) { + return { + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + } + } + return { + ok: false, + result: null, + diagnostic: { + code: 'specialized-normalization-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} diff --git a/src/vm/jsconfuser-vm/normalize-string-concealing.js b/src/vm/jsconfuser-vm/normalize-string-concealing.js new file mode 100644 index 00000000..988fab61 --- /dev/null +++ b/src/vm/jsconfuser-vm/normalize-string-concealing.js @@ -0,0 +1,564 @@ +import { selectContinuationLoads } from './finally-continuations.js' +import { HELPER_SIGNATURES } from './string-concealing-helper-signatures.js' +import { diagnoseWordcode } from './read-wordcode.js' +import { diagnoseReferences } from './validate-references.js' +import { diagnoseFunctionPartition } from './partition-functions.js' + +class StringConcealingDecline extends Error { + constructor(message) { + super(message) + this.code = 'string-concealing-shape' + } +} + +function decline(message) { + throw new StringConcealingDecline(message) +} + +const TARGET_OFFSETS = Object.freeze({ + JUMP: [1], + JUMP_IF_FALSE: [2], + JUMP_IF_TRUE: [2], + FOR_IN_NEXT: [3], + TRY_SETUP: [1], + FINALLY_SETUP: [1, 4], + MAKE_CLOSURE: [2], +}) + +// The helper's trace in execution order: walk from its entry, follow every jump, and number +// instructions by first visit. A conditional records the instructions each outcome runs next, and +// reaching an instruction already traced records a GOTO. Layout and jumps to the next instruction +// leave no trace, so any earlier decoder's order gives the template's trace (CFF reorders bodies). +function helperTrace(items, pool, start = 0) { + const byPc = new Map(items.map((item) => [item.pc, item])) + const end = items.at(-1) ? items.at(-1).pc + items.at(-1).words.length : start + const follow = (pc) => { + const seen = new Set() + while (byPc.get(pc)?.name === 'JUMP' && !seen.has(pc)) { + seen.add(pc) + pc = byPc.get(pc).words[1] + } + if (pc < start || pc >= end || !byPc.has(pc)) + decline(`String helper control leaves its body at ${pc}`) + return pc + } + const value = (index) => { + const raw = pool ? pool[index] : index + // concealConstants' XOR restores this u32 literal in signed form; + // the template's subsequent BOR 0 applies the same 32-bit coercion. + if (raw === -1640531527) return 0x9e3779b9 + return raw === undefined ? '' : raw + } + const registers = new Map() + const read = (register) => registers.get(register) ?? `arg${register}` + const order = new Map() + const trace = [] + const branches = [] + const pending = [start] + while (pending.length) { + let pc = follow(pending.pop()) + for (;;) { + if (order.has(pc)) { + trace.push(['GOTO', pc]) + break + } + const item = byPc.get(pc) + const w = item.words + order.set(pc, trace.length) + if ( + ((item.name === 'LOAD_CONST' || item.name === 'LOAD_GLOBAL') && + w[3] !== 0) || + (item.name === 'CALL' && w[3] !== w.length - 4) || + (item.name === 'CALL_METHOD' && w[4] !== w.length - 5) + ) + decline(`String helper operand width changed at ${item.pc}`) + const next = item.pc + w.length + if (item.name === 'JUMP_IF_FALSE' || item.name === 'JUMP_IF_TRUE') { + const target = follow(w[2]) + const fallthrough = follow(next) + const [whenTrue, whenFalse] = + item.name === 'JUMP_IF_FALSE' + ? [fallthrough, target] + : [target, fallthrough] + const branch = ['IF', read(w[1]), whenTrue, whenFalse] + trace.push(branch) + branches.push(branch) + pending.push(whenFalse) + pc = whenTrue + continue + } + if (item.name === 'RETURN') { + trace.push(['RETURN', read(w[1])]) + break + } + let expression + if (item.name === 'LOAD_THIS') expression = ['this'] + else if (item.name === 'LOAD_CONST') + expression = ['constant', value(w[2])] + else if (item.name === 'LOAD_GLOBAL') expression = ['global', value(w[2])] + else if (item.name === 'MOVE') expression = ['move', read(w[2])] + else if (item.name === 'GET_PROP') + expression = ['get', read(w[2]), read(w[3])] + else if (item.name === 'CALL') + expression = ['call', read(w[2]), ...w.slice(4).map(read)] + else if (item.name === 'CALL_METHOD') + expression = ['method', read(w[2]), read(w[3]), ...w.slice(5).map(read)] + else if ( + ['ADD', 'BOR', 'BXOR', 'BAND', 'SHL', 'USHR', 'LT'].includes(item.name) + ) + expression = [item.name, read(w[2]), read(w[3])] + else decline(`Unexpected ${item.name} in string helper`) + trace.push([item.name, expression]) + registers.set( + w[1], + item.name === 'MOVE' ? expression[1] : `op${trace.length - 1}`, + ) + pc = follow(next) + } + } + for (const entry of trace) + if (entry[0] === 'GOTO') entry[1] = order.get(entry[1]) + for (const branch of branches) { + branch[2] = order.get(branch[2]) + branch[3] = order.get(branch[3]) + } + return trace +} + +// A function's instructions in execution order: from its entry, follow every jump (dropping it), +// take a conditional's fallthrough first and its other targets later, then append what no walk +// reached in layout order. A finally continuation load names the label the finalizer's JUMP_REG +// resumes at, so it is a target like any jump's. Instructions that run one after another stay +// adjacent whatever the layout, so prologue and call-site reads do not depend on it. +// `droppedBefore` collects, for each instruction, the jumps followed just before reaching it. +function executionOrder(items, start, droppedBefore = new Map()) { + const byPc = new Map(items.map((item) => [item.pc, item])) + const continuationLoads = selectContinuationLoads( + items.map((item) => ({ + key: item.pc, + functionId: 0, + name: item.name, + words: item.words, + })), + ) + const placed = new Set() + const order = [] + const pending = [start] + while (pending.length) { + let pc = pending.pop() + let jumps = [] + while (byPc.has(pc) && !placed.has(pc)) { + const item = byPc.get(pc) + placed.add(pc) + if (item.name === 'JUMP') { + jumps.push(item.pc) + pc = item.words[1] + continue + } + if (jumps.length) droppedBefore.set(item.pc, jumps) + jumps = [] + order.push(item) + for (const offset of TARGET_OFFSETS[item.name] ?? []) + if (item.name !== 'MAKE_CLOSURE') pending.push(item.words[offset]) + if (continuationLoads.has(item.pc)) pending.push(item.words[2]) + if (['RETURN', 'THROW', 'JUMP_REG'].includes(item.name)) break + pc = item.pc + item.words.length + } + } + for (const item of items) + if (!placed.has(item.pc) && item.name !== 'JUMP') order.push(item) + return order +} + +function templateTrace(signature) { + let pc = 0 + const items = signature.map(([name, ...operands]) => { + const item = { name, pc, words: [null, ...operands] } + pc += item.words.length + return item + }) + return helperTrace(items, null) +} + +const EXPECTED_HELPER_TRACES = HELPER_SIGNATURES.map(templateTrace) + +function decodeBank(value) { + if ( + typeof value !== 'string' || + !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test( + value, + ) + ) + decline('Bank is not base64') + const bytes = Buffer.from(value, 'base64') + if (bytes.toString('base64') !== value || bytes.length % 2 !== 0) + decline('Bank is not canonical u16 LE base64') + return bytes +} + +function decodeSlice(bank, key, start, length) { + if ( + !Number.isInteger(key) || + key < 1 || + key > 0xffffffff || + !Number.isInteger(start) || + start < 100 || + !Number.isInteger(length) || + length < 0 || + start + length > bank.length / 2 - 100 + ) + decline('Decode slice is outside the padded bank') + let state = key + let result = '' + for (let index = 0; index < length; index += 1) { + state = (state + 0x9e3779b9) | 0 + const mask = (state ^ (state >>> 13)) & 0xffff + result += String.fromCharCode( + (bank[2 * (start + index)] | (bank[2 * (start + index) + 1] << 8)) ^ mask, + ) + } + return result +} + +function recognize(container, wordcode, references) { + const instructions = wordcode.instructions + const closures = instructions.filter((item) => item.name === 'MAKE_CLOSURE') + const starts = [ + 0, + ...new Set(references.frame.descriptors.map((item) => item.startPc)), + ].sort((a, b) => a - b) + if (starts.length < 3) decline('String helper entries are absent') + const inflateStart = starts.at(-2) + const decodeStart = starts.at(-1) + const pool = container.roles.pool.values + for (const [start, end, expected] of [ + [inflateStart, decodeStart, EXPECTED_HELPER_TRACES[0]], + [decodeStart, wordcode.wordCount, EXPECTED_HELPER_TRACES[1]], + ]) { + const observed = helperTrace( + instructions.filter((item) => item.pc >= start && item.pc < end), + pool, + start, + ) + if (JSON.stringify(observed) !== JSON.stringify(expected)) + decline(`Pinned helper body at ${start} changed`) + } + const inflateSites = closures.filter((item) => item.words[2] === inflateStart) + const decodeSites = closures.filter((item) => item.words[2] === decodeStart) + if ( + inflateSites.length !== 1 || + inflateSites[0].pc !== 0 || + inflateSites[0].functionMeta.paramCount !== 1 || + inflateSites[0].functionMeta.regCount < 12 || + inflateSites[0].functionMeta.captureCount !== 0 || + decodeSites.length === 0 || + decodeSites.some( + (item) => + item.functionMeta.paramCount !== 4 || + item.functionMeta.regCount < 14 || + item.functionMeta.captureCount !== 0, + ) + ) + decline('String helper closure sites changed') + const program = executionOrder( + instructions.filter((item) => item.pc < starts[1]), + 0, + ) + const bankLoad = program[1] + const bankCall = program[2] + if ( + bankLoad?.name !== 'LOAD_CONST' || + bankLoad.words[3] !== 0 || + bankCall?.name !== 'CALL' || + bankCall.words.length !== 5 || + bankCall.words[2] !== inflateSites[0].words[1] || + bankCall.words[3] !== 1 || + bankCall.words[4] !== bankLoad.words[1] + ) + decline('Program bank inflation prologue changed') + const bank = decodeBank(pool[bankLoad.words[2]]) + const bankRegister = bankCall.words[1] + const functionPlans = new Map() + const removed = new Set([inflateSites[0].pc, bankLoad.pc, bankCall.pc]) + const replacements = new Map() + const siteRunJumps = new Map() + const specialRegisters = new Map() + const parentByChild = new Map() + const normalClosures = closures.filter( + (item) => item.words[2] !== inflateStart && item.words[2] !== decodeStart, + ) + for (const site of normalClosures) { + const owner = starts.findLast((start) => start <= site.pc) + const previous = parentByChild.get(site.words[2]) + if (previous !== undefined && previous !== owner) + decline('User closure has ambiguous parents') + parentByChild.set(site.words[2], owner) + } + const users = new Set() + for (let index = 0; index < starts.length - 2; index += 1) { + const start = starts[index] + const end = starts[index + 1] + const droppedBefore = new Map() + const body = executionOrder( + instructions.filter((item) => item.pc >= start && item.pc < end), + start, + droppedBefore, + ) + const isRoot = index === 0 + let cursor = isRoot ? 3 : 0 + let localBank = isRoot ? bankRegister : null + if ( + !isRoot && + body[0]?.name === 'LOAD_UPVALUE' && + body[1]?.name === 'MAKE_CLOSURE' && + body[1].words[2] === decodeStart + ) { + localBank = body[0].words[1] + removed.add(body[0].pc) + cursor = 1 + } + const decoder = body[cursor] + if (decoder?.name === 'MAKE_CLOSURE' && decoder.words[2] === decodeStart) { + users.add(start) + removed.add(decoder.pc) + cursor += 1 + if (localBank === null) decline('Decoder has no inherited bank') + const decoderRegister = decoder.words[1] + const keyRegister = decoderRegister + 1 + const startRegister = decoderRegister + 2 + const lengthRegister = decoderRegister + 3 + specialRegisters.set( + start, + new Set([ + localBank, + decoderRegister, + keyRegister, + startRegister, + lengthRegister, + ]), + ) + let siteCount = 0 + for (let at = cursor; at < body.length; at += 1) { + const item = body[at] + if (item.name !== 'CALL' || item.words[2] !== decoderRegister) continue + const triplet = body.slice(at - 3, at) + if ( + triplet.length !== 3 || + triplet.some( + (part, offset) => + part.name !== 'LOAD_INT' || + part.words[1] !== + [keyRegister, startRegister, lengthRegister][offset], + ) || + item.words.length !== 8 || + item.words[3] !== 4 || + item.words[4] !== localBank || + item.words[5] !== keyRegister || + item.words[6] !== startRegister || + item.words[7] !== lengthRegister + ) + decline(`Decode call at ${item.pc} changed`) + const plain = decodeSlice( + bank, + triplet[0].words[2], + triplet[1].words[2], + triplet[2].words[2], + ) + const poolIndex = pool.length + replacements.size + replacements.set(triplet[0].pc, { + name: 'LOAD_CONST', + words: [ + container.roles.op.values.LOAD_CONST, + item.words[1], + poolIndex, + 0, + ], + plain, + }) + for (const part of [...triplet.slice(1), item]) { + // A jump between two parts of this site (a flattened block split the site) is the + // site's own fallthrough: after the collapse it continues after the call. + for (const jump of droppedBefore.get(part.pc) ?? []) + siteRunJumps.set(jump, item.pc + item.words.length) + } + for (const part of [...triplet.slice(1), item]) removed.add(part.pc) + siteCount += 1 + } + if (!siteCount) decline('Decoder closure has no string sites') + } else if (localBank !== null && !isRoot) { + decline('Bank upvalue has no decoder') + } + // The encoder emits the prologue (root: inflate closure, bank load, inflate call; a user: bank + // upvalue load; then the decode closure) straight-line, so a jump into one of its parts (a + // flattened block split the run) continues at the first instruction after it. + for (const part of body.slice(0, cursor)) + for (const jump of droppedBefore.get(part.pc) ?? []) + siteRunJumps.set(jump, body[cursor].pc) + functionPlans.set(start, { start, end, body, isRoot, localBank }) + } + if (decodeSites.length !== users.size) + decline('Decoder prologue count changed') + const need = new Set(users) + for (const user of users) { + let parent = parentByChild.get(user) + while (parent !== undefined && !need.has(parent)) { + need.add(parent) + parent = parentByChild.get(parent) + } + } + if (!need.has(0)) decline('String users are detached from the program') + const closureReplacements = new Map() + for (const site of normalClosures) { + const child = site.words[2] + if (!need.has(child)) continue + const parent = parentByChild.get(child) + const count = site.words[5] + const source = + parent === 0 + ? [1, bankRegister] + : [0, closures.find((item) => item.words[2] === parent)?.words[5] - 1] + if ( + count < 1 || + site.words.at(-2) !== source[0] || + site.words.at(-1) !== source[1] + ) + decline(`Bank capture at ${site.pc} changed`) + const raw = site.words.slice(0, -2) + raw[5] -= 1 + closureReplacements.set(site.pc, raw) + } + for (const user of users) { + if (user === 0) continue + const plan = functionPlans.get(user) + const descriptor = closures.find((item) => item.words[2] === user) + if (!descriptor || plan.body[0].words[2] !== descriptor.words[5] - 1) + decline(`Bank upvalue index in function ${user} changed`) + } + for (const plan of functionPlans.values()) { + const special = + specialRegisters.get(plan.start) ?? + (plan.isRoot + ? new Set([bankRegister, inflateSites[0].words[1], bankLoad.words[1]]) + : new Set()) + if (plan.isRoot) { + special.add(inflateSites[0].words[1]) + special.add(bankLoad.words[1]) + special.add(bankRegister) + } + for (const item of plan.body) { + if (removed.has(item.pc) || replacements.has(item.pc)) continue + const operands = item.operands.filter( + (operand) => operand.kind === 'register', + ) + if (operands.some((operand) => special.has(operand.index))) + decline(`String helper register escapes at ${item.pc}`) + if ( + item.name === 'MAKE_CLOSURE' && + item.captures.some( + (capture, index) => + capture.kind === 'local' && + special.has(capture.index) && + !( + closureReplacements.has(item.pc) && + index === item.captures.length - 1 + ), + ) + ) + decline(`Bank capture escapes at ${item.pc}`) + } + } + for (const item of instructions) { + if (item.pc >= inflateStart) removed.add(item.pc) + } + const parts = [] + const pcMap = new Map() + let nextPc = 0 + for (const item of instructions) { + if (item.pc >= inflateStart) continue + const replacement = replacements.get(item.pc) + if (replacement) { + pcMap.set(item.pc, nextPc) + parts.push({ originPc: item.pc, pc: nextPc, ...replacement }) + nextPc += replacement.words.length + } else if (removed.has(item.pc)) { + if (starts.includes(item.pc)) pcMap.set(item.pc, nextPc) + } else { + pcMap.set(item.pc, nextPc) + const words = closureReplacements.get(item.pc) ?? item.words.slice() + parts.push({ originPc: item.pc, pc: nextPc, name: item.name, words }) + nextPc += words.length + } + } + pcMap.set(wordcode.wordCount, nextPc) + const remap = (pc, part) => { + if (!pcMap.has(pc) && siteRunJumps.has(part.originPc)) + return remap(siteRunJumps.get(part.originPc), part) + if (!pcMap.has(pc)) decline(`Target ${pc} was removed with string helpers`) + return pcMap.get(pc) + } + const continuationLoads = selectContinuationLoads( + parts.map((part) => ({ + key: part.pc, + functionId: starts.findLast((start) => start <= part.originPc), + name: part.name, + words: part.words, + })), + ) + const words = parts.flatMap((part) => { + const raw = part.words.slice() + for (const offset of TARGET_OFFSETS[part.name] ?? []) + raw[offset] = remap(raw[offset], part) + if (continuationLoads.has(part.pc)) raw[2] = remap(raw[2], part) + return raw + }) + const normalized = structuredClone(container) + normalized.roles.words.values = words + for (const { plain } of replacements.values()) + normalized.roles.pool.values.push(plain) + const afterB = diagnoseWordcode(normalized) + if (!afterB.ok) decline(afterB.diagnostic.message) + const afterC = diagnoseReferences(normalized, afterB.result) + if (!afterC.ok) decline(afterC.diagnostic.message) + const afterD = diagnoseFunctionPartition( + normalized, + afterB.result, + afterC.result, + ) + if (!afterD.ok) decline(afterD.diagnostic.message) + return { + schemaVersion: 'jsconfuser-vm-string-concealing-normalization.v1', + container: normalized, + wordcode: afterB.result, + references: afterC.result, + functions: afterD.result, + pcMap, + decodedCount: replacements.size, + threadedFunctionCount: need.size - 1, + } +} + +export function diagnoseNormalizeStringConcealing( + container, + wordcode, + references, +) { + try { + return { + ok: true, + result: recognize(container, wordcode, references), + diagnostic: null, + } + } catch (error) { + return { + ok: false, + result: null, + diagnostic: { + code: + error instanceof StringConcealingDecline + ? error.code + : 'string-concealing-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} diff --git a/src/vm/jsconfuser-vm/observe-aliased-runtime.js b/src/vm/jsconfuser-vm/observe-aliased-runtime.js new file mode 100644 index 00000000..0711df95 --- /dev/null +++ b/src/vm/jsconfuser-vm/observe-aliased-runtime.js @@ -0,0 +1,90 @@ +import { parse } from '@babel/parser' +import { partitionHandlers } from './locate-handlers.js' +import { requireIdentifiedHandlers } from './identify-handlers.js' +import { + ContainerDecline, + PARSER_OPTIONS, + decline, + deepFreeze, +} from './container-role-helpers.js' + +function recognize( + ast, + { + antiObservation = null, + specializedObservation = null, + macroObservation = null, + } = {}, +) { + const view = requireIdentifiedHandlers(ast, 'alias-runtime-container') + const { canonical, extra } = partitionHandlers(view) + if ( + canonical.length !== Object.keys(view.opcodes).length || + extra.length === 0 + ) { + decline('alias-runtime', 'Canonical or alias cases are incomplete') + } + const antiPrefixCount = antiObservation?.syntheticCases.length ?? 0 + const specializedPrefixCount = specializedObservation?.specialized.length ?? 0 + const macroPrefixCount = macroObservation?.macros.length ?? 0 + // The classifier (identify-handlers.js) sorts the added handlers by what they do; an alias is one + // canonical handler reading its operands in another order (`canonicalToStored[j]` is the read + // canonical operand j takes). Every other added handler belongs to an earlier observation. + const aliases = [] + for (const handler of extra) { + const found = view.added.get(handler.label) + if (found?.kind !== 'alias') continue + if (!handler.ends) decline('alias-runtime', 'Alias case form changed') + aliases.push({ + opcode: handler.label, + originalOpcode: found.opcode, + originalName: found.name, + arity: found.canonicalToStored.length, + canonicalToStored: found.canonicalToStored, + }) + } + if (aliases.length === 0) + decline('alias-runtime', 'No alias opcode cases remain') + if ( + extra.length - aliases.length !== + antiPrefixCount + specializedPrefixCount + macroPrefixCount + ) + decline('alias-runtime', 'Unproved extra opcode cases remain') + return deepFreeze({ + schemaVersion: 'jsconfuser-vm-aliased-runtime-observation.v1', + antiPrefixCount, + specializedPrefixCount, + macroPrefixCount, + aliases, + }) +} + +export function diagnoseAliasedRuntime(input, options) { + try { + const ast = typeof input === 'string' ? parse(input, PARSER_OPTIONS) : input + if (ast?.type !== 'File' && ast?.type !== 'Program') { + decline('invalid-input', 'Expected source text or a Babel AST') + } + return deepFreeze({ + ok: true, + result: recognize(ast, options), + diagnostic: null, + }) + } catch (error) { + if (error instanceof ContainerDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'alias-runtime-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} diff --git a/src/vm/jsconfuser-vm/observe-dispatcher-wordcode.js b/src/vm/jsconfuser-vm/observe-dispatcher-wordcode.js new file mode 100644 index 00000000..4978f57e --- /dev/null +++ b/src/vm/jsconfuser-vm/observe-dispatcher-wordcode.js @@ -0,0 +1,610 @@ +const MATH = Symbol('pinned Math global') +const IMUL = Symbol('pinned Math.imul method') +const RECEIVER = Symbol('unused decode receiver') + +export class DispatcherDecline extends Error { + constructor(code, message) { + super(message) + this.code = code + } +} + +function decline(code, message) { + throw new DispatcherDecline(code, message) +} + +function requireShape(value, message) { + if (!value) decline('dispatcher-shape', message) + return value +} + +function number(value) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + decline('dispatcher-decode', 'Decode closure used a nonnumeric value') + } + return value +} + +function evaluateClosure(instructions, descriptor, pool, args) { + if ( + descriptor.captureCount !== 0 || + descriptor.hasRest || + descriptor.paramCount !== args.length || + descriptor.regCount < args.length + ) { + decline('dispatcher-closure', 'Decode closure has unexpected metadata') + } + const registers = Array(descriptor.regCount).fill(undefined) + args.forEach((value, index) => { + registers[index] = value + }) + const read = (index) => { + if (!Number.isInteger(index) || index < 0 || index >= registers.length) { + decline('dispatcher-closure', 'Decode closure reads outside its frame') + } + return registers[index] + } + const write = (index, value) => { + if (!Number.isInteger(index) || index < 0 || index >= registers.length) { + decline('dispatcher-closure', 'Decode closure writes outside its frame') + } + registers[index] = value + } + let result + let returned = false + for (let index = 0; index < instructions.length; index += 1) { + const instruction = instructions[index] + const w = instruction.words + if (instruction.name === 'RETURN') { + result = read(w[1]) + returned = true + const tail = instructions.slice(index + 1) + if ( + tail.length !== 2 || + tail[0].name !== 'LOAD_CONST' || + tail[0].words[3] !== 0 || + pool[tail[0].words[2]] !== undefined || + tail[1].name !== 'RETURN' || + tail[1].words[1] !== tail[0].words[1] + ) { + decline('dispatcher-closure', 'Decode closure has an unexpected tail') + } + break + } + if (instruction.name === 'LOAD_THIS') { + write(w[1], RECEIVER) + } else if (instruction.name === 'LOAD_INT') { + write(w[1], w[2]) + } else if (instruction.name === 'LOAD_CONST') { + if (w[3] !== 0 || w[2] >= pool.length) { + decline('dispatcher-closure', 'Decode closure has an invalid constant') + } + write(w[1], pool[w[2]]) + } else if (instruction.name === 'LOAD_GLOBAL') { + if (w[3] !== 0 || pool[w[2]] !== 'Math') { + decline('dispatcher-closure', 'Decode closure reads another global') + } + write(w[1], MATH) + } else if (instruction.name === 'GET_PROP') { + if (read(w[2]) !== MATH || read(w[3]) !== 'imul') { + decline('dispatcher-closure', 'Decode closure reads another property') + } + write(w[1], IMUL) + } else if (instruction.name === 'CALL_METHOD') { + if ( + read(w[2]) !== MATH || + read(w[3]) !== IMUL || + w[4] !== 2 || + w.length !== 7 + ) { + decline('dispatcher-closure', 'Decode closure calls another method') + } + write(w[1], Math.imul(number(read(w[5])), number(read(w[6])))) + } else if (instruction.name === 'MOVE') { + write(w[1], read(w[2])) + } else if (instruction.name === 'UNARY_BITNOT') { + write(w[1], ~number(read(w[2]))) + } else if ( + instruction.name === 'ADD' || + instruction.name === 'SUB' || + instruction.name === 'BOR' || + instruction.name === 'BXOR' || + instruction.name === 'SHL' || + instruction.name === 'USHR' + ) { + const left = number(read(w[2])) + const right = number(read(w[3])) + const value = { + ADD: () => left + right, + SUB: () => left - right, + BOR: () => left | right, + BXOR: () => left ^ right, + SHL: () => left << right, + USHR: () => left >>> right, + }[instruction.name]() + write(w[1], value) + } else if (instruction.name === 'BUILD_ARRAY') { + if (w[2] !== 1 || w.length !== 4) { + decline('dispatcher-closure', 'Decode array has unexpected width') + } + write(w[1], { kind: 'array', value: read(w[3]) }) + } else if (instruction.name === 'BUILD_OBJECT') { + if (w[2] !== 1 || w.length !== 5 || typeof read(w[3]) !== 'string') { + decline('dispatcher-closure', 'Decode object has unexpected width') + } + write(w[1], { + kind: 'object', + key: read(w[3]), + value: read(w[4]), + }) + } else { + decline( + 'dispatcher-closure', + `Decode closure uses unsupported ${instruction.name}`, + ) + } + } + if (!returned) decline('dispatcher-closure', 'Decode closure has no return') + return result +} + +function instructionsFor(fn, byPc) { + return fn.instructionPcs.map((pc) => + requireShape(byPc.get(pc), `Function ${fn.id} has a missing instruction`), + ) +} + +function readDispatcherTail(ownerInstructions, hoist, closure, pool) { + const jump = ownerInstructions.at(-1) + requireShape( + jump?.name === 'JUMP_REG', + 'Dispatcher block does not end in JUMP_REG', + ) + const disp = jump.words[1] + let callIndex = ownerInstructions.length - 2 + let resultMode = { kind: 'direct' } + const get = ownerInstructions[callIndex] + if (get?.name === 'GET_PROP') { + const load = ownerInstructions[callIndex - 1] + requireShape( + get.words[1] === disp && + get.words[2] === disp && + load?.words[1] === get.words[3], + 'Dispatcher result unwrap is disconnected', + ) + if (load.name === 'LOAD_INT' && load.words[2] === 0) { + resultMode = { kind: 'array' } + } else if ( + load.name === 'LOAD_CONST' && + load.words[3] === 0 && + typeof pool[load.words[2]] === 'string' + ) { + resultMode = { kind: 'object', key: pool[load.words[2]] } + } else { + decline('dispatcher-shape', 'Dispatcher result key is not pinned') + } + callIndex -= 2 + } + const call = ownerInstructions[callIndex] + requireShape( + call?.name === 'CALL' && + call.words[1] === disp && + call.words[2] === hoist.words[1] && + call.words[3] === closure.descriptor.paramCount && + (call.words[3] === 2 || call.words[3] === 3) && + call.words.length === call.words[3] + 4, + 'Dispatcher call is not pinned', + ) + const args = call.words.slice(4) + let keyRegisters + if (args.length === 2 && args.filter((item) => item === disp).length === 1) { + keyRegisters = args.filter((item) => item !== disp) + } else if ( + args.length === 3 && + args[0] === disp && + args[1] !== disp && + args[2] !== disp && + args[1] !== args[2] + ) { + keyRegisters = args.slice(1) + } else { + decline('dispatcher-shape', 'Dispatcher argument order is not pinned') + } + return { + call, + disp, + args, + keyRegisters, + resultMode, + block: ownerInstructions.slice(callIndex), + } +} + +function routeAt( + ownerInstructions, + jumpIndex, + tail, + closureInstructions, + closure, + pool, + decodedTargetMap, + suspects, +) { + const jump = ownerInstructions[jumpIndex] + if (jump.name !== 'JUMP' || jump.words[1] !== tail.call.pc) return null + let cursor = jumpIndex - 1 + const keyParts = [] + for (const register of [...tail.keyRegisters].reverse()) { + const load = ownerInstructions[cursor--] + requireShape( + load?.name === 'LOAD_INT' && load.words[1] === register, + `Dispatcher route at ${jump.pc} has no site key`, + ) + keyParts.unshift(load.words[2]) + } + const key = + keyParts.length === 1 + ? keyParts[0] + : (keyParts[0] | (keyParts[1] << 16)) >>> 0 + if ( + key === 0 || + (keyParts.length === 2 && keyParts.some((part) => part > 0xffff)) + ) { + decline( + 'dispatcher-key', + `Dispatcher route at ${jump.pc} has an invalid key`, + ) + } + // decodeOrigin gives the address the encoder wrote (an origin PC); decode reads it in this + // stream's space. + const decodeOrigin = (encoded) => { + const args = tail.args.map((register) => + register === tail.disp + ? encoded + : keyParts[tail.keyRegisters.indexOf(register)], + ) + const result = evaluateClosure( + closureInstructions, + closure.descriptor, + pool, + args, + ) + const mode = tail.resultMode + const value = + mode.kind === 'direct' + ? result + : result?.kind === mode.kind && + (mode.kind === 'array' || result.key === mode.key) + ? result.value + : null + if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) { + decline( + 'dispatcher-target', + `Dispatcher route at ${jump.pc} has no numeric target`, + ) + } + return value + } + const decode = (encoded) => { + const value = decodeOrigin(encoded) + if (!decodedTargetMap) return value + if (!decodedTargetMap.has(value)) { + decline('dispatcher-target', 'Decoded target has no anti PC boundary') + } + return decodedTargetMap.get(value) + } + const last = ownerInstructions[cursor] + if (last?.name === 'LOAD_INT' && last.words[1] === tail.disp) { + return { + startPc: last.pc, + endPc: jump.nextPc, + oldPcs: ownerInstructions + .slice(cursor, jumpIndex + 1) + .map((item) => item.pc), + name: 'JUMP', + target: decode(last.words[2]), + } + } + requireShape( + last?.name === 'ADD' && + last.words[1] === tail.disp && + last.words[2] === tail.disp, + `Dispatcher route at ${jump.pc} has no pinned selector`, + ) + const delta = last.words[3] + const select = ownerInstructions[--cursor] + requireShape( + (select?.name === 'MUL' || select?.name === 'BAND') && + select.words[1] === delta && + ((select.words[2] === delta && select.words[3] !== delta) || + (select.words[3] === delta && select.words[2] !== delta)), + `Dispatcher route at ${jump.pc} has an invalid selector`, + ) + const test = select.words[2] === delta ? select.words[3] : select.words[2] + const sub = ownerInstructions[--cursor] + requireShape( + sub?.name === 'SUB' && + sub.words[1] === delta && + sub.words[2] === delta && + sub.words[3] === tail.disp, + `Dispatcher route at ${jump.pc} has an invalid delta`, + ) + const other = ownerInstructions[--cursor] + const base = ownerInstructions[--cursor] + requireShape( + other?.name === 'LOAD_INT' && + other.words[1] === delta && + base?.name === 'LOAD_INT' && + base.words[1] === tail.disp, + `Dispatcher route at ${jump.pc} has invalid endpoints`, + ) + if (select.name === 'BAND') { + const neg = ownerInstructions[--cursor] + requireShape( + neg?.name === 'UNARY_NEG' && + neg.words[1] === test && + neg.words[2] === test, + `Dispatcher route at ${jump.pc} has no mask`, + ) + } + const positive = ownerInstructions[--cursor] + const invert = ownerInstructions[--cursor] + requireShape( + positive?.name === 'UNARY_POS' && + positive.words[1] === test && + positive.words[2] === test && + invert?.name === 'UNARY_NOT' && + invert.words[1] === test, + `Dispatcher route at ${jump.pc} has no pinned condition`, + ) + const baseTarget = decode(base.words[2]) + const otherTarget = decode(other.words[2]) + // The skip label follows the JUMP, past any suspects selfModifying planted after it; while they + // are undecided, the label may sit on any of them. + const after = + ownerInstructions.slice(jumpIndex + 1).find(({ pc }) => !suspects.has(pc)) + ?.pc ?? jump.nextPc + const skipped = (target) => + target === jump.nextPc || + (target > jump.nextPc && + target <= after && + (target === after || suspects.has(target))) + if (baseTarget === otherTarget) { + return { + startPc: invert.pc, + endPc: jump.nextPc, + oldPcs: ownerInstructions + .slice(cursor, jumpIndex + 1) + .map((item) => item.pc), + name: 'JUMP', + target: baseTarget, + } + } + if (skipped(baseTarget) === skipped(otherTarget)) { + decline( + 'dispatcher-target', + `Dispatcher route at ${jump.pc} has no unique skip edge`, + ) + } + return { + startPc: invert.pc, + endPc: jump.nextPc, + oldPcs: ownerInstructions + .slice(cursor, jumpIndex + 1) + .map((item) => item.pc), + name: skipped(baseTarget) ? 'JUMP_IF_FALSE' : 'JUMP_IF_TRUE', + condition: invert.words[2], + target: skipped(baseTarget) ? otherTarget : baseTarget, + // The JUMP_IF falls through where the JUMP skipped to; a later owner settling what lies between + // needs this label. + skipOrigin: decodeOrigin((skipped(baseTarget) ? base : other).words[2]), + } +} + +// A PATCH suspect (an undecided selfModifying filler, planted after a terminator) is read past +// where this decoder relies on what follows a terminator (the closure tail, the instruction before +// the dispatcher block, a route's skip label) and by the scans for dispatcher registers. Routes +// match over every instruction, as a hidden label can sit on a suspect that starts one. Suspects +// stay in the output as they are. +function recognize(container, wordcode, functions, decodedTargetMap, origin) { + const suspects = new Set( + origin.map((pc) => (decodedTargetMap ? decodedTargetMap.get(pc) : pc)), + ) + const byPc = new Map(wordcode.instructions.map((item) => [item.pc, item])) + const byStart = new Map( + functions.functions.map((item) => [item.startPc, item]), + ) + const ownerByPc = new Map( + functions.ownership.map((item) => [item.pc, item.functionId]), + ) + const plans = [] + for (const owner of functions.functions) { + const instructions = instructionsFor(owner, byPc) + const hoist = instructions[0] + if ( + hoist.name !== 'MAKE_CLOSURE' || + instructions.at(-1).name !== 'JUMP_REG' + ) + continue + requireShape( + hoist.words[5] === 0 && hoist.words[6] === 0, + `Function ${owner.id} has no dispatcher closure hoist`, + ) + const closure = byStart.get(hoist.words[2]) + requireShape( + closure?.descriptor?.creationPc === hoist.pc && + closure.descriptor.paramCount === hoist.words[3], + `Function ${owner.id} has no unique decoder closure`, + ) + const closureInstructions = instructionsFor(closure, byPc).filter( + ({ pc }) => !suspects.has(pc), + ) + const tail = readDispatcherTail( + instructions, + hoist, + closure, + container.roles.pool.values, + ) + const callIndex = instructions.findIndex((item) => item === tail.call) + const before = instructions + .slice(0, callIndex) + .findLast(({ pc }) => !suspects.has(pc)) + requireShape( + callIndex > 0 && + ['RETURN', 'THROW', 'JUMP', 'JUMP_REG'].includes(before?.name), + `Function ${owner.id} falls into its dispatcher block`, + ) + const closureRegister = hoist.words[1] + for (const instruction of instructions) { + if (instruction === hoist || instruction === tail.call) continue + if (suspects.has(instruction.pc)) continue + if ( + instruction.wordOperands.some( + (operand) => + operand.kind === 'register' && operand.index === closureRegister, + ) + ) { + decline( + 'dispatcher-closure', + `Function ${owner.id} uses its decode closure outside dispatch`, + ) + } + } + const routes = [] + for (let index = 0; index < instructions.length; index += 1) { + const route = routeAt( + instructions, + index, + tail, + closureInstructions, + closure, + container.roles.pool.values, + decodedTargetMap, + suspects, + ) + if (route) routes.push(route) + } + requireShape( + routes.length > 0, + `Function ${owner.id} has no dispatcher routes`, + ) + const used = new Set() + for (const route of routes) { + for (const pc of route.oldPcs) { + if (used.has(pc)) + decline('dispatcher-shape', 'Dispatcher routes overlap') + used.add(pc) + } + if ( + !byPc.has(route.target) || + ownerByPc.get(route.target) !== owner.id || + route.target >= tail.call.pc || + (route.name !== 'JUMP' && !byPc.has(route.endPc)) + ) { + decline('dispatcher-target', 'Decoded target leaves the original body') + } + } + const erasedRegisters = new Set([ + closureRegister, + tail.disp, + ...tail.keyRegisters, + ]) + const erasedPcs = new Set([ + hoist.pc, + ...tail.block.map((item) => item.pc), + ...used, + ]) + for (const instruction of instructions) { + if ( + erasedPcs.has(instruction.pc) && + !['JUMP', 'JUMP_REG'].includes(instruction.name) + ) + erasedRegisters.add(instruction.words[1]) + } + for (const route of routes) { + if (route.condition !== undefined && erasedRegisters.has(route.condition)) + decline('dispatcher-shape', 'Dispatcher condition uses erased state') + } + for (const instruction of instructions) { + if (erasedPcs.has(instruction.pc) || suspects.has(instruction.pc)) + continue + if ( + instruction.operands.some( + (operand) => + operand.kind === 'register' && erasedRegisters.has(operand.index), + ) || + instruction.captures?.some( + (capture) => + capture.kind === 'local' && erasedRegisters.has(capture.index), + ) + ) + decline( + 'dispatcher-shape', + `Dispatcher register escapes into payload at ${instruction.pc}`, + ) + } + plans.push({ + ownerId: owner.id, + closureId: closure.id, + hoistPc: hoist.pc, + dispatcherPcs: tail.block.map((item) => item.pc), + routes, + }) + } + if (plans.length === 0) decline('no-dispatcher', 'No dispatcher block exists') + const closureIds = new Set(plans.map((item) => item.closureId)) + if (closureIds.size !== plans.length) { + decline('dispatcher-closure', 'Dispatcher closure is shared by owners') + } + const functionById = new Map( + functions.functions.map((item) => [item.id, item]), + ) + for (const plan of plans) { + const references = wordcode.instructions.filter( + (item) => + item.name === 'MAKE_CLOSURE' && + functionById.get(plan.closureId)?.startPc === item.words[2], + ) + if (references.length !== 1 || references[0].pc !== plan.hoistPc) { + decline('dispatcher-closure', 'Decode closure has another creation site') + } + } + return { + schemaVersion: 'jsconfuser-vm-dispatcher-observation.v1', + plans, + } +} + +export function diagnoseDispatcherWordcode( + container, + wordcode, + functions, + decodedTargetMap, + suspects = [], +) { + try { + return { + ok: true, + result: recognize( + container, + wordcode, + functions, + decodedTargetMap, + suspects, + ), + diagnostic: null, + } + } catch (error) { + return { + ok: false, + result: null, + diagnostic: { + code: + error instanceof DispatcherDecline + ? error.code + : 'dispatcher-observation-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} diff --git a/src/vm/jsconfuser-vm/observe-macro-runtime.js b/src/vm/jsconfuser-vm/observe-macro-runtime.js new file mode 100644 index 00000000..199c7301 --- /dev/null +++ b/src/vm/jsconfuser-vm/observe-macro-runtime.js @@ -0,0 +1,121 @@ +import { parse } from '@babel/parser' +import { partitionHandlers } from './locate-handlers.js' +import { requireIdentifiedHandlers } from './identify-handlers.js' +import { + ContainerDecline, + PARSER_OPTIONS, + decline, + deepFreeze, +} from './container-role-helpers.js' + +const ALWAYS_EXCLUDED = new Set([ + 'PATCH', + 'TRY_SETUP', + 'TRY_END', + 'DEBUGGER', + 'MAKE_CLOSURE', +]) +const NONTERMINAL_EXCLUDED = new Set([ + 'RETURN', + 'CALL', + 'CALL_METHOD', + 'NEW', + 'THROW', + 'JUMP', + 'JUMP_IF_FALSE', + 'JUMP_IF_TRUE', + 'FOR_IN_NEXT', + 'FINALLY_SETUP', +]) + +function recognize( + ast, + { + antiObservation = null, + specializedObservation = null, + allowAliasTail = false, + } = {}, +) { + const view = requireIdentifiedHandlers(ast, 'macro-runtime-container') + const { canonical, extra } = partitionHandlers(view) + if ( + canonical.length !== Object.keys(view.opcodes).length || + extra.length === 0 + ) { + decline('macro-runtime', 'Canonical or macro cases are incomplete') + } + const antiPrefixCount = antiObservation?.syntheticCases.length ?? 0 + const specializedPrefixCount = specializedObservation?.specialized.length ?? 0 + // The classifier (identify-handlers.js) sorts the added handlers by what they do; a macro is two + // or more canonical handlers in sequence, reading their operands as they are used. The producer + // never puts an excluded opcode in a macro, and a nonterminal-excluded one only last. + const macros = [] + for (const handler of extra) { + const found = view.added.get(handler.label) + if (found?.kind !== 'macro') continue + const { steps } = found + const reads = steps.flatMap((step) => step.reads) + if ( + !handler.ends || + steps.length < 2 || + steps.length > 5 || + reads.some((read, index) => read !== index) || + steps.some( + (step, index) => + ALWAYS_EXCLUDED.has(step.name) || + (NONTERMINAL_EXCLUDED.has(step.name) && index !== steps.length - 1), + ) + ) + decline( + 'macro-runtime', + 'Macro case has no unique canonical decomposition', + ) + macros.push({ + opcode: handler.label, + steps: steps.map(({ name, opcode }) => ({ name, opcode })), + }) + } + if (macros.length === 0) + decline('macro-runtime', 'No macro opcode cases remain') + if ( + !allowAliasTail && + extra.length - macros.length !== antiPrefixCount + specializedPrefixCount + ) + decline('macro-runtime', 'Unproved extra opcode cases remain') + return deepFreeze({ + schemaVersion: 'jsconfuser-vm-macro-runtime-observation.v1', + antiPrefixCount, + specializedPrefixCount, + macros, + }) +} + +export function diagnoseMacroRuntime(input, options) { + try { + const ast = typeof input === 'string' ? parse(input, PARSER_OPTIONS) : input + if (ast?.type !== 'File' && ast?.type !== 'Program') { + decline('invalid-input', 'Expected source text or a Babel AST') + } + return deepFreeze({ + ok: true, + result: recognize(ast, options), + diagnostic: null, + }) + } catch (error) { + if (error instanceof ContainerDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'macro-runtime-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} diff --git a/src/vm/jsconfuser-vm/observe-patch-added-opcodes.js b/src/vm/jsconfuser-vm/observe-patch-added-opcodes.js new file mode 100644 index 00000000..9ac2397e --- /dev/null +++ b/src/vm/jsconfuser-vm/observe-patch-added-opcodes.js @@ -0,0 +1,88 @@ +import { parse } from '@babel/parser' +import { PARSER_OPTIONS } from './container-role-helpers.js' +import { locateHandlers, partitionHandlers } from './locate-handlers.js' +import { behaviourTable } from './identify-handlers.js' +import { diagnoseAntiRuntime } from './analyze-anti-runtime.js' +import { diagnoseSpecializedRuntime } from './observe-specialized-runtime.js' +import { diagnoseMacroRuntime } from './observe-macro-runtime.js' +import { diagnoseAliasedRuntime } from './observe-aliased-runtime.js' + +// Allocation order is anti, specialized, macro, alias. Prefix observations can +// stop at a later option's added opcodes; the last observer must prove the entire suffix. +export function diagnosePatchAddedOpcodes(source) { + const antiDiagnosis = diagnoseAntiRuntime(source, { + allowSpecializedTail: true, + }) + const anti = antiDiagnosis.ok ? antiDiagnosis.result : null + const specializedOptions = { + allowAntiPrefix: Boolean(anti), + allowMacroTail: true, + } + const specializedDiagnosis = diagnoseSpecializedRuntime( + source, + specializedOptions, + ) + const specialized = specializedDiagnosis.ok + ? specializedDiagnosis.result + : null + const macroOptions = { + antiObservation: anti, + specializedObservation: specialized, + allowAliasTail: true, + } + const macroDiagnosis = diagnoseMacroRuntime(source, macroOptions) + const macro = macroDiagnosis.ok ? macroDiagnosis.result : null + const aliasDiagnosis = diagnoseAliasedRuntime(source, { + antiObservation: anti, + specializedObservation: specialized, + macroObservation: macro, + }) + const alias = aliasDiagnosis.ok ? aliasDiagnosis.result : null + const complete = aliasDiagnosis.ok + ? aliasDiagnosis + : macro + ? diagnoseMacroRuntime(source, { ...macroOptions, allowAliasTail: false }) + : specialized + ? diagnoseSpecializedRuntime(source, { + ...specializedOptions, + allowMacroTail: false, + }) + : anti + ? diagnoseAntiRuntime(source) + : null + if (complete && !complete.ok) return complete + if (!complete) { + // An unused damaged added case still belongs to the enclosing runtime. + // Do not admit its canonical payload by silently forgetting the suffix. + try { + const view = locateHandlers(parse(source, PARSER_OPTIONS)) + const { extra } = partitionHandlers({ + ...view, + opcodes: behaviourTable(view).opcodes, + }) + if (extra.length > 0) + return { + ok: false, + result: null, + diagnostic: { + code: 'patch-added-opcode-runtime', + message: 'The enclosing dispatch has unproved added opcode cases', + }, + } + } catch (error) { + return { + ok: false, + result: null, + diagnostic: { + code: error.code ?? 'patch-added-opcode-runtime', + message: error.message, + }, + } + } + } + return { + ok: true, + result: { anti, specialized, macro, alias }, + diagnostic: null, + } +} diff --git a/src/vm/jsconfuser-vm/observe-self-modifying.js b/src/vm/jsconfuser-vm/observe-self-modifying.js new file mode 100644 index 00000000..a0f0c638 --- /dev/null +++ b/src/vm/jsconfuser-vm/observe-self-modifying.js @@ -0,0 +1,359 @@ +import { diagnoseObservedWordcodeInstruction } from './read-wordcode.js' +import { selectContinuationLoads } from './finally-continuations.js' + +const SCHEMA = 'jsconfuser-vm-self-modifying-observation.v2' +const TERMINATORS = new Set(['JUMP', 'RETURN', 'THROW', 'JUMP_REG']) +const DIRECT_TARGET_OFFSETS = Object.freeze({ + JUMP: [1], + JUMP_IF_FALSE: [2], + JUMP_IF_TRUE: [2], + FOR_IN_NEXT: [3], + TRY_SETUP: [1], + FINALLY_SETUP: [1, 4], + MAKE_CLOSURE: [2], +}) +// The encoder's decoy filler shapes: no constant reader, no transfer, no call. +const FILLER_SHAPES = new Set([ + 'TRY_END', + 'LOAD_THIS', + 'MOVE', + 'LOAD_INT', + 'UNARY_NEG', + 'UNARY_NOT', + 'UNARY_BITNOT', + 'TYPEOF', + 'ADD', + 'SUB', + 'MUL', + 'DIV', + 'MOD', + 'BAND', + 'BOR', + 'BXOR', + 'SHL', + 'SHR', + 'USHR', + 'LT', + 'GT', + 'LTE', + 'GTE', + 'EQ', + 'NEQ', + 'GET_PROP', +]) +const DECOY_RUN_MAX = 2 + +class ObservationDecline extends Error { + constructor(code, message) { + super(message) + this.code = code + } +} + +function decline(code, message) { + throw new ObservationDecline(code, message) +} + +function intersects(a, b) { + return a.start < b.end && b.start < a.end +} + +function decrypt(words, destPc, sourceStart, sourceEnd, key) { + let streamKey = (key ^ destPc) | 0 + return words.slice(sourceStart, sourceEnd).map((word) => { + streamKey = (streamKey + 0x9e3779b9) | 0 + return (word ^ (streamKey ^ (streamKey >>> 13))) >>> 0 + }) +} + +function withWords(container, words) { + return { + ...container, + roles: { + ...container.roles, + words: { ...container.roles.words, values: words }, + }, + } +} + +function requireParsed(diagnosis, code) { + if (!diagnosis.ok) { + decline(code, diagnosis.diagnostic.message) + } + return diagnosis.result +} + +// An added opcode ends control when its last canonical step does; fused anti steps never do. +function terminates(instruction) { + if (TERMINATORS.has(instruction.name)) return true + return ( + instruction.name !== 'SYNTHETIC' && + TERMINATORS.has(instruction.steps?.at(-1)?.name) + ) +} + +function stepsOf(instruction) { + return instruction.steps && instruction.name !== 'SYNTHETIC' + ? instruction.steps + : [instruction] +} + +// After a canonical terminator, selfModifying may plant up to two decoy entries (a decoy PATCH or a +// filler shape) before the next block's label. The run ends at the first block start this stage +// can see (direct targets, closure entries, handler targets, finally continuation literals), at a +// real PATCH, or at any instruction no filler can be. While its budget lasts, selfModifying +// patches a block's whole body, so a block it reached starts with a PATCH or its own terminator. +// JUMP_REG is the only operation that takes the PC from data. The compiler's finalizer JUMP_REG +// reads its FINALLY_SETUP continuation register, so the continuation loads are all its targets. +// When every JUMP_REG is such a finalizer, the visible starts are every label and a matched entry +// is a filler. Otherwise a JUMP_REG (the dispatcher's) hides labels only a later decoder recovers: +// a matched entry is a suspect, kept as code and settled by this rule once that decoder has run. +function matchFillers(code) { + const visible = new Set([0]) + const starts = [0] + for (const instruction of code) { + for (const step of stepsOf(instruction)) { + for (const offset of DIRECT_TARGET_OFFSETS[step.name] ?? []) + visible.add(step.words[offset]) + if (step.name === 'MAKE_CLOSURE') starts.push(step.words[2]) + } + } + starts.sort((left, right) => left - right) + const functionOf = (pc) => starts.findLastIndex((start) => start <= pc) + const items = code.flatMap((instruction) => + stepsOf(instruction).map((step) => ({ + key: step, + functionId: functionOf(instruction.pc), + name: step.name, + words: step.words, + })), + ) + for (const load of selectContinuationLoads(items)) visible.add(load.words[2]) + const continuations = new Set( + items + .filter(({ name }) => name === 'FINALLY_SETUP') + .map(({ functionId, words }) => `${functionId}:${words[2]}`), + ) + const complete = items.every( + ({ name, functionId, words }) => + name !== 'JUMP_REG' || continuations.has(`${functionId}:${words[1]}`), + ) + const runs = [] + for (let index = 0; index < code.length; index++) { + if (!TERMINATORS.has(code[index].name)) continue + const run = [] + runs.push(run) + for ( + let next = index + 1, entries = 0; + next < code.length && entries < DECOY_RUN_MAX; + next++, entries++ + ) { + const entry = code[next] + if (visible.has(entry.pc) || entry.name === 'PATCH') break + if (entry.name === 'DECOY') continue + if (!FILLER_SHAPES.has(entry.name)) break + run.push(entry.pc) + } + } + const matched = runs.flat() + return complete + ? { fillers: matched, suspects: [], suspectRuns: [] } + : { + fillers: [], + suspects: matched, + suspectRuns: runs.filter((run) => run.length > 0), + } +} + +// selfModifying moves runs of code into encrypted trailing storage and leaves, at the execution +// site, a five-word PATCH followed by garbage of the run's width. Its decoys copy junk to junk +// inside that storage. Read the execution words in order from PC 0: a real PATCH targets its own +// next word and copies from storage after that destination, so materialize it and continue at the +// destination; skip any other PATCH as a decoy. Storage starts no later than the lowest address +// any PATCH header names, wherever the header sits. Code ends after the last terminator before +// that whose operand compiled code could hold: a function never falls off its end, and storage's +// only terminator-like word is an arena block's random last word, whose operand belongs to the +// next item. Nothing is interpreted beyond widths, terminators and those operands. +export function diagnoseSelfModifyingRegions( + container, + addedOpcodes = {}, + { requirePatch = true } = {}, +) { + try { + const original = container?.roles?.words?.values + if (!Array.isArray(original) || original.length === 0) { + decline('invalid-container', 'Numeric word stream is missing') + } + const PATCH = container.roles.op.values.PATCH + const words = original.slice() + const instructions = [] + const patches = [] + const decoys = [] + const sources = [] + const destinations = [] + let storageStart = words.length + let unparsedPc = null + let storageFailure = null + let pc = 0 + while (pc < storageStart) { + if (words[pc] === PATCH) { + if (pc + 5 > words.length) { + decline('truncated-patch', `PATCH at ${pc} has no full header`) + } + const header = words.slice(pc, pc + 5) + if ( + !header.every( + (word) => Number.isInteger(word) && word >= 0 && word <= 0xffffffff, + ) + ) { + decline('invalid-patch-word', `PATCH at ${pc} has a non-u32 word`) + } + const [, destPc, sourceStart, sourceEnd, key] = header + const width = sourceEnd - sourceStart + if ( + destPc !== pc + 5 || + width <= 0 || + sourceStart < destPc + width || + sourceEnd > words.length + ) { + decoys.push({ pc, destPc, sourceStart, sourceEnd }) + // Decoys address only storage, including addresses behind the sweep. + storageStart = Math.min(storageStart, destPc, sourceStart) + instructions.push({ pc, name: 'DECOY', nextPc: pc + 5, width: 5 }) + pc += 5 + continue + } + const destination = { start: destPc, end: destPc + width } + const source = { start: sourceStart, end: sourceEnd } + const overlaps = [...sources, ...destinations].some( + (interval) => + intersects(interval, destination) || intersects(interval, source), + ) + const clear = decrypt(original, destPc, sourceStart, sourceEnd, key) + const clearContainer = withWords(container, clear) + const parsed = [] + let unparsed = overlaps ? 'patch-overlap' : null + for (let offset = 0; offset < width && !unparsed; ) { + const step = diagnoseObservedWordcodeInstruction( + clearContainer, + offset, + addedOpcodes, + ) + if (!step.ok) unparsed = 'patch-ciphertext' + else { + parsed.push(step.result) + offset = step.result.nextPc + } + } + // A real region always decrypts to whole instructions and overlaps nothing; a header + // that fails either is an arena decoy, so storage starts here. + if (unparsed) { + storageFailure = { code: unparsed, pc } + storageStart = Math.min(storageStart, pc) + break + } + words.splice(destPc, width, ...clear) + sources.push(source) + destinations.push(destination) + patches.push({ + pc, + destPc, + sourceStart, + sourceEnd, + width, + instructions: parsed.map(({ name, pc: offset }) => ({ + pc: destPc + offset, + name, + })), + }) + storageStart = Math.min(storageStart, sourceStart) + instructions.push({ pc, name: 'PATCH', nextPc: destPc, width: 5 }) + pc = destPc + continue + } + const parsed = diagnoseObservedWordcodeInstruction( + withWords(container, words), + pc, + addedOpcodes, + ) + // Only storage fails to parse here; the end rule below drops it. + if (!parsed.ok) { + unparsedPc = pc + break + } + // Code never overlaps storage: an instruction reaching past the bound is junk. + if (parsed.result.nextPc > storageStart) break + instructions.push(parsed.result) + pc = parsed.result.nextPc + } + // An arena block's last word is a random canonical opcode reading its operand from the next + // item; a terminator whose operand no compiled instruction could hold is that word, not code. + const starts = new Set(instructions.map(({ pc }) => pc)) + const frames = [ + [0, container.roles.scalars.MAIN_REG_COUNT?.value ?? Infinity], + ] + for (const instruction of instructions) + for (const step of stepsOf(instruction)) + if (step.name === 'MAKE_CLOSURE') + frames.push([step.words[2], step.words[4]]) + frames.sort((left, right) => left[0] - right[0]) + const frameOf = (pc) => + frames.findLast(([start]) => start <= pc)?.[1] ?? Infinity + const possible = (instruction) => { + if (!TERMINATORS.has(instruction.name)) return true + if (instruction.name === 'JUMP') return starts.has(instruction.words[1]) + return instruction.words[1] < frameOf(instruction.pc) + } + const last = instructions.findLastIndex( + (instruction) => + terminates(instruction) && + instruction.nextPc <= storageStart && + possible(instruction), + ) + if (last < 0) { + if (storageFailure) + decline( + storageFailure.code, + `PATCH at ${storageFailure.pc} has no code region`, + ) + decline('no-code-end', 'No terminator ends the code before storage') + } + const code = instructions.slice(0, last + 1) + const end = code.at(-1).nextPc + if (requirePatch && patches.length === 0) { + // Without a PATCH, storage is undefined: an unparsed word proves nothing about absence. + if (unparsedPc !== null) + decline('unparsed-word', `Word at ${unparsedPc} does not parse`) + decline('no-patch', 'No PATCH precedes storage') + } + const { fillers, suspects, suspectRuns } = matchFillers(code) + return { + ok: true, + result: { + schemaVersion: SCHEMA, + patches: patches.filter((patch) => patch.pc < end), + decoys: decoys.filter((decoy) => decoy.pc < end), + fillers, + suspects, + suspectRuns, + instructions: code, + end, + storageStart, + sourceIntervals: sources.sort((a, b) => a.start - b.start), + destinationIntervals: destinations.sort((a, b) => a.start - b.start), + patchedWords: words, + }, + diagnostic: null, + } + } catch (error) { + return { + ok: false, + result: null, + diagnostic: { + code: + error instanceof ObservationDecline ? error.code : 'observer-error', + message: error instanceof Error ? error.message : String(error), + }, + } + } +} diff --git a/src/vm/jsconfuser-vm/observe-specialized-runtime.js b/src/vm/jsconfuser-vm/observe-specialized-runtime.js new file mode 100644 index 00000000..42ffcc19 --- /dev/null +++ b/src/vm/jsconfuser-vm/observe-specialized-runtime.js @@ -0,0 +1,94 @@ +import { parse } from '@babel/parser' +import { partitionHandlers } from './locate-handlers.js' +import { requireIdentifiedHandlers } from './identify-handlers.js' +import { + ContainerDecline, + PARSER_OPTIONS, + decline, + deepFreeze, +} from './container-role-helpers.js' + +function recognize( + ast, + { allowAntiPrefix = false, allowMacroTail = false } = {}, +) { + const view = requireIdentifiedHandlers(ast, 'specialized-runtime-container') + const { canonical, extra } = partitionHandlers(view) + if ( + canonical.length !== Object.keys(view.opcodes).length || + extra.length === 0 + ) { + decline( + 'specialized-runtime', + 'Canonical or specialized cases are incomplete', + ) + } + // The classifier (identify-handlers.js) sorts the added handlers by what they do; a specialized + // handler is one canonical handler with every operand baked in, in canonical read order. + const kindOf = (handler) => view.added.get(handler.label)?.kind ?? null + const antiPrefixCount = allowAntiPrefix + ? extra.filter((handler) => kindOf(handler) === 'anti').length + : 0 + const specialized = [] + for (const handler of extra) { + const found = view.added.get(handler.label) + if (found?.kind !== 'specialized') continue + if (!handler.ends) + decline('specialized-runtime', 'Specialized case form changed') + // Each baked operand becomes a word of the expanded instruction. + if ( + !found.operands.every( + (value) => Number.isInteger(value) && value >= 0 && value <= 0xffffffff, + ) + ) + decline( + 'specialized-runtime', + `Specialized case ${handler.label} operand is not a word`, + ) + specialized.push({ + opcode: handler.label, + originalName: found.name, + originalOpcode: found.opcode, + operands: found.operands, + }) + } + if (specialized.length === 0) + decline('specialized-runtime', 'No specialized opcode cases remain') + if (!allowMacroTail && specialized.length !== extra.length - antiPrefixCount) + decline('specialized-runtime', 'Unproved extra opcode cases remain') + return deepFreeze({ + schemaVersion: 'jsconfuser-vm-specialized-runtime-observation.v1', + antiPrefixCount, + specialized, + }) +} + +export function diagnoseSpecializedRuntime(input, options) { + try { + const ast = typeof input === 'string' ? parse(input, PARSER_OPTIONS) : input + if (ast?.type !== 'File' && ast?.type !== 'Program') { + decline('invalid-input', 'Expected source text or a Babel AST') + } + return deepFreeze({ + ok: true, + result: recognize(ast, options), + diagnostic: null, + }) + } catch (error) { + if (error instanceof ContainerDecline) { + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + } + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'specialized-runtime-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} diff --git a/src/vm/jsconfuser-vm/string-concealing-helper-signatures.js b/src/vm/jsconfuser-vm/string-concealing-helper-signatures.js new file mode 100644 index 00000000..41d697c5 --- /dev/null +++ b/src/vm/jsconfuser-vm/string-concealing-helper-signatures.js @@ -0,0 +1,81 @@ +// Bytecode compilation of the two pinned stringConcealing.ts helper templates. +// Pool indexes and absolute branch PCs are normalized before comparison. +export const HELPER_SIGNATURES = [ + [ + ['LOAD_THIS', 2], + ['LOAD_GLOBAL', 6, 'atob', 0], + ['CALL', 7, 6, 1, 0], + ['MOVE', 3, 7], + ['LOAD_CONST', 6, '', 0], + ['MOVE', 4, 6], + ['LOAD_CONST', 6, 0, 0], + ['MOVE', 5, 6], + ['LOAD_CONST', 6, 'length', 0], + ['GET_PROP', 7, 3, 6], + ['LT', 6, 5, 7], + ['JUMP_IF_FALSE', 6, 129], + ['LOAD_GLOBAL', 6, 'String', 0], + ['LOAD_CONST', 7, 'fromCharCode', 0], + ['GET_PROP', 8, 6, 7], + ['LOAD_CONST', 7, 'charCodeAt', 0], + ['GET_PROP', 9, 3, 7], + ['CALL_METHOD', 7, 3, 9, 1, 5], + ['LOAD_CONST', 9, 'charCodeAt', 0], + ['GET_PROP', 10, 3, 9], + ['LOAD_CONST', 9, 1, 0], + ['ADD', 11, 5, 9], + ['CALL_METHOD', 9, 3, 10, 1, 11], + ['LOAD_CONST', 10, 8, 0], + ['SHL', 11, 9, 10], + ['BOR', 9, 7, 11], + ['CALL_METHOD', 7, 6, 8, 1, 9], + ['ADD', 6, 4, 7], + ['MOVE', 4, 6], + ['LOAD_CONST', 6, 2, 0], + ['ADD', 7, 5, 6], + ['MOVE', 5, 7], + ['JUMP', 28], + ['RETURN', 4], + ['LOAD_CONST', 6, '', 0], + ['RETURN', 6], + ], + [ + ['LOAD_THIS', 5], + ['LOAD_CONST', 9, '', 0], + ['MOVE', 6, 9], + ['LOAD_CONST', 9, 0, 0], + ['MOVE', 7, 9], + ['LT', 9, 7, 3], + ['JUMP_IF_FALSE', 9, 128], + ['LOAD_CONST', 9, 2654435769, 0], + ['ADD', 10, 1, 9], + ['LOAD_CONST', 9, 0, 0], + ['BOR', 11, 10, 9], + ['MOVE', 1, 11], + ['LOAD_CONST', 9, 13, 0], + ['USHR', 10, 1, 9], + ['BXOR', 9, 1, 10], + ['LOAD_CONST', 10, 65535, 0], + ['BAND', 11, 9, 10], + ['MOVE', 8, 11], + ['LOAD_GLOBAL', 9, 'String', 0], + ['LOAD_CONST', 10, 'fromCharCode', 0], + ['GET_PROP', 11, 9, 10], + ['LOAD_CONST', 10, 'charCodeAt', 0], + ['GET_PROP', 12, 0, 10], + ['ADD', 10, 2, 7], + ['CALL_METHOD', 13, 0, 12, 1, 10], + ['BXOR', 10, 13, 8], + ['CALL_METHOD', 12, 9, 11, 1, 10], + ['ADD', 9, 6, 12], + ['MOVE', 6, 9], + ['MOVE', 9, 7], + ['LOAD_CONST', 9, 1, 0], + ['ADD', 10, 7, 9], + ['MOVE', 7, 10], + ['JUMP', 16], + ['RETURN', 6], + ['LOAD_CONST', 9, '', 0], + ['RETURN', 9], + ], +] diff --git a/test/vm/jsconfuser-vm/analyze-anti-register-flow.test.js b/test/vm/jsconfuser-vm/analyze-anti-register-flow.test.js new file mode 100644 index 00000000..0607f069 --- /dev/null +++ b/test/vm/jsconfuser-vm/analyze-anti-register-flow.test.js @@ -0,0 +1,144 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { diagnoseAntiRuntime } from '../../../src/vm/jsconfuser-vm/analyze-anti-runtime.js' +import { diagnoseAntiRegisterFlow } from '../../../src/vm/jsconfuser-vm/analyze-anti-register-flow.js' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseAntiWordStream } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' + +function fixture(name) { + return fs.readFileSync( + new URL(`fixtures/${name}/encoded.js`, import.meta.url), + 'utf8', + ) +} + +function observed(name) { + const source = fixture(name) + const extracted = diagnoseContainer(source).result + const runtime = diagnoseAntiRuntime(source).result + expect(extracted).not.toBeNull() + expect(runtime).not.toBeNull() + return { extracted, runtime } +} + +test('identifies real steps from closed fake register flow', () => { + const { extracted, runtime } = observed('anti-runtime-boot') + const result = diagnoseAntiRegisterFlow(extracted, runtime) + expect(result).toMatchObject({ + ok: true, + diagnostic: null, + result: { + schemaVersion: 'jsconfuser-vm-anti-register-flow-observation.v1', + functionCount: 1, + fusedFunctionCount: 1, + realSteps: [ + { opcode: 61, index: 2, name: 'MUL' }, + { opcode: 62, index: 2, name: 'ADD' }, + { opcode: 63, index: 2, name: 'MOD' }, + ], + }, + }) + expect(Object.isFrozen(result.result.realSteps)).toBe(true) +}) + +test('selects the real ADD when a synthetic handler has two ADD steps', () => { + const { extracted, runtime } = observed('anti-runtime-duplicate-step') + const result = diagnoseAntiRegisterFlow(extracted, runtime) + expect(result.result.realSteps).toEqual([ + { opcode: 61, index: 0, name: 'MUL' }, + { opcode: 62, index: 0, name: 'ADD' }, + { opcode: 63, index: 2, name: 'MOD' }, + ]) +}) + +test('declines cross-pool fake writes, changed fake jumps, and damaged seeds', () => { + const boot = observed('anti-runtime-boot') + const bootStream = diagnoseAntiWordStream(boot.extracted, boot.runtime).result + const case61 = bootStream.instructions.find( + ({ opcode }) => opcode.value === 61, + ) + const crossPool = structuredClone(boot.extracted) + const storedPosition = boot.runtime.syntheticCases[0].canonicalToStored[0] + crossPool.roles.words.values[case61.pc + 1 + storedPosition] = 8 + expect(diagnoseAntiRegisterFlow(crossPool, boot.runtime)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'register-isolation' }, + }) + + const duplicate = observed('anti-runtime-duplicate-step') + const duplicateStream = diagnoseAntiWordStream( + duplicate.extracted, + duplicate.runtime, + ).result + const case62 = duplicateStream.instructions.find( + ({ opcode }) => opcode.value === 62, + ) + const jump = structuredClone(duplicate.extracted) + const jumpCanonical = + duplicate.runtime.syntheticCases[1].steps[1].operandStart + 1 + const jumpStored = + duplicate.runtime.syntheticCases[1].canonicalToStored[jumpCanonical] + jump.roles.words.values[case62.pc + 1 + jumpStored] = case62.nextPc + 1 + expect(diagnoseAntiRegisterFlow(jump, duplicate.runtime)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'register-isolation' }, + }) + + const case63 = duplicateStream.instructions.find( + ({ opcode }) => opcode.value === 63, + ) + const fakeConstant = structuredClone(duplicate.extracted) + const constantStored = + duplicate.runtime.syntheticCases[2].canonicalToStored[1] + fakeConstant.roles.words.values[case63.pc + 1 + constantStored] = 6 + expect( + diagnoseAntiRegisterFlow(fakeConstant, duplicate.runtime), + ).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'register-isolation' }, + }) + + const seed = structuredClone(boot.extracted) + seed.roles.words.values[1] = 8 + expect(diagnoseAntiRegisterFlow(seed, boot.runtime)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'register-isolation' }, + }) +}) + +// Pins the historical FOR_IN_NEXT word tags as the flow consumer reads them: +// the iterator operand is checked as a direct target and the exit operand is not. +test('reads appended FOR_IN_NEXT operands with their historical tags', () => { + const lines = fixture('anti-runtime-boot').split('\n') + const line = lines.findIndex((text) => text.startsWith('var BYTECODE = [')) + const words = JSON.parse( + lines[line].slice('var BYTECODE = '.length).replace(/;\s*$/, ''), + ) + const flow = (iter, exit) => { + const changed = [...lines] + changed[line] = `var BYTECODE = ${JSON.stringify([ + ...words, + ...[53, 0, iter, exit, 45, 0], + ])};` + const source = changed.join('\n') + const extracted = diagnoseContainer(source) + const runtime = diagnoseAntiRuntime(source) + expect(extracted.ok).toBe(true) + expect(runtime.ok).toBe(true) + return diagnoseAntiRegisterFlow(extracted.result, runtime.result) + } + expect(flow(1, 87)).toMatchObject({ + ok: false, + result: null, + diagnostic: { + code: 'function-boundary', + message: 'Direct target crosses a function interval', + }, + }) + expect(flow(0, 87).ok).toBe(true) + expect(flow(0, 1).ok).toBe(true) +}) diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/encoded.js b/test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/encoded.js new file mode 100644 index 00000000..5d7a77d1 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/encoded.js @@ -0,0 +1,1094 @@ +var CONSTANTS = [/* 0 */"5Q4Z5hc1", /* 1 */57752, /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */6, /* 5 */7, /* 6 */3, /* 7 */5, /* 8 */undefined]; +var BYTECODE = [0, 2, 0, 0, 0, 3, 1, 0, 4, 1, 2, 4, 2, 0, 0, 5, 3, 0, 0, 6, 4, 0, 0, 7, 5, 0, 61, 2, 8, 7, 2, 3, 6, 3, 2, 3, 2, 0, 3, 2, 0, 6, 6, 0, 62, 2, 2, 3, 6, 3, 3, 2, 3, 7, 3, 3, 2, 3, 8, 2, 0, 6, 7, 0, 63, 7, 6, 3, 3, 2, 8, 2, 2, 9, 4, 5, 8, 0, 4, 8, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MUL_3_6_8_11_0_7_2_1_12_4_10_5_9 (order: [3,6,8,11,0,7,2,1,12,4,10,5,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[11], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[12], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[8]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[9]; + var pairCount = _operands[10]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[11]]; + var val = regs[base + _operands[12]]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + case 62: + { + // ANTI_ADD_11_5_3_8_0_12_1_10_6_4_13_2_14_7_9 (order: [11,5,3,8,0,12,1,10,6,4,13,2,14,7,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[11], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[13], _unsortedOperands[3], _unsortedOperands[14], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[12]]; + // SHL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a << regs[base + _operands[2]]; + // LTE + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a <= regs[base + _operands[5]]; + // ADD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a + regs[base + _operands[8]]; + // SUB + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a - regs[base + _operands[11]]; + // BAND + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a & regs[base + _operands[14]]; + break; + } + case 63: + { + // ANTI_MOD_6_7_2_3_1_5_4_0 (order: [6,7,2,3,1,5,4,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[1]]; + // TYPEOF + var dst = _operands[0]; + regs[base + dst] = typeof regs[base + _operands[1]]; + // LOOSE_NEQ + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a != regs[base + _operands[4]]; + // MOD + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a % regs[base + _operands[7]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/qualification.json b/test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/qualification.json new file mode 100644 index 00000000..3f697617 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "arithmetic--antiInstrumentation--1", + "case": "arithmetic", + "profile": "antiInstrumentation", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "wordcode-declined", + "message": "readWordcode declined the input wordcode" + }, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": null, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/source.js b/test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-boot/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/encoded.js b/test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/encoded.js new file mode 100644 index 00000000..7ac55f7f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/encoded.js @@ -0,0 +1,1090 @@ +var CONSTANTS = [/* 0 */0, /* 1 */60840, /* 2 */62183, /* 3 */46799, /* 4 */"NGc00Ht3", /* 5 */"window", /* 6 */"TEST_OUTPUT", /* 7 */6, /* 8 */7, /* 9 */"OMS6", /* 10 */3, /* 11 */5, /* 12 */46403, /* 13 */undefined]; +var BYTECODE = [0, 6, 0, 0, 0, 5, 1, 0, 0, 2, 2, 0, 0, 3, 3, 0, 0, 4, 4, 0, 4, 1, 2, 7, 5, 0, 0, 8, 6, 0, 0, 9, 7, 0, 0, 10, 8, 0, 61, 0, 3, 2, 2, 11, 9, 2, 3, 3, 2, 2, 4, 10, 2, 9, 0, 9, 10, 0, 62, 3, 6, 67, 10, 3, 9, 11, 3, 0, 9, 11, 0, 63, 2, 3, 0, 9, 11, 12, 2, 10, 2, 9, 7, 8, 11, 0, 7, 13, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MUL_8_6_4_3_0_7_12_10_14_13_9_5_2_11_1 (order: [8,6,4,3,0,7,12,10,14,13,9,5,2,11,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[14], _unsortedOperands[12], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[11], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[13], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[8]]; + // MUL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a * regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[6]; + var name = this._constant(_operands[7], _operands[8]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // USHR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a >>> regs[base + _operands[11]]; + // LOOSE_EQ + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a == regs[base + _operands[14]]; + break; + } + case 62: + { + // ANTI_ADD_5_3_4_0_6_2_1_7 (order: [5,3,4,0,6,2,1,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[7]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[3]; + var target = _operands[4]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // ADD + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a + regs[base + _operands[7]]; + break; + } + case 63: + { + // ANTI_MOD_4_5_2_8_6_1_3_7_0 (order: [4,5,2,8,6,1,3,7,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[3]]; + // LOAD_CONST + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[3]; + var count = _operands[4]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[5]]; + regs[base + dst] = elems; + // MOD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a % regs[base + _operands[8]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/qualification.json b/test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/qualification.json new file mode 100644 index 00000000..587ed32b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "arithmetic--antiInstrumentation--2", + "case": "arithmetic", + "profile": "antiInstrumentation", + "seed": 2, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "wordcode-declined", + "message": "readWordcode declined the input wordcode" + }, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": null, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/source.js b/test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-duplicate-step/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-boot/encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-boot/encoded.js new file mode 100644 index 00000000..411807b4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-boot/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = [56, 5, 185, 207, 2570702713, 34, 3, 23, 4, 14, 32, 3, 23, 55, 11, 24, 15, 11, 56, 130, 313, 326, 2781205869, 26, 14, 14, 10, 56, 32, 278, 302, 2641224562, 23, 21, 9, 18, 35, 3, 22, 27, 12, 16, 10, 56, 58, 127, 140, 3377748332, 34, 7, 18, 56, 308, 320, 325, 1751006225, 45, 2, 1, 17, 18115, 36, 13, 23, 56, 309, 302, 313, 1631177435, 35, 2, 24, 55, 5, 9, 15, 12, 17, 3, 7, 19, 13, 1, 4, 35, 3, 16, 55, 56, 313, 255, 266, 2769174992, 27, 0, 21, 22, 56, 63, 141, 151, 359116224, 12, 0, 12, 13, 25, 12, 19, 1, 22, 5, 21, 19, 56, 324, 172, 173, 1445349015, 4, 21, 56, 315, 315, 320, 501818177, 16, 5, 19, 6, 27, 21, 0, 4, 56, 62, 262, 274, 1076355419, 23, 19, 5, 2, 56, 245, 174, 179, 3904579366, 19, 16, 5, 9, 34, 6, 23, 32, 8, 1, 56, 107, 239, 243, 1322630380, 16, 10, 17, 8, 56, 131, 60, 73, 2713225533, 55, 14, 21, 16, 8, 18, 0, 23, 13, 17, 16, 3, 1, 930149051, 3584978857, 1944298404, 303507167, 2957685344, 1317330407, 3971565178, 2331101931, 690773908, 3345132434, 1704928911, 64199344, 2718952884, 1077966217, 3732865692, 2092076674, 451425153, 3106248572, 1465213572, 4119395724, 2478986651, 838847125, 56, 375, 82, 91, 2699531026, 24, 2, 21, 13, 25, 12, 8, 9, 17, 21, 5, 22, 22, 3, 7, 8, 12, 12, 11, 8, 42, 56, 72, 309, 315, 653214154, 17, 3, 6, 1, 25, 5, 24, 17, 36, 16, 23, 21, 22, 20, 23, 56, 312, 109, 114, 4019635200, 32, 16, 23, 56, 80, 304, 311, 1392964937, 16, 13, 1, 20, 36, 4, 7, 36, 3, 16, 4, 8, 1000636449, 3654948902, 2014810331, 374044776, 3028740094, 1387800696, 4042633462, 2402142291, 761283034, 3416193101, 1775456448, 134731714, 2788827200, 1148496066, 3802730588, 2162260429, 521928821, 3176241397, 1535717457, 4190415835, 2550030505, 909371560, 3564073401, 1923275168, 18, 18, 10, 24, 15, 3, 10, 6, 56, 244, 315, 322, 819795777, 56, 239, 136, 149, 2826645300, 22, 4, 18, 11, 4, 24, 56, 152, 234, 243, 1463903744, 34, 3, 6, 36, 15, 14, 56, 131, 250, 263, 2081029858, 56, 307, 174, 184, 2613096358, 13, 9, 1, 15, 16, 0, 7, 2, 8, 22, 12, 19, 55, 27, 1, 8, 20, 13, 18, 20, 15, 10, 23, 16, 11, 2, 26, 11, 22, 24, 34, 15, 15, 26, 9, 22, 1, 35, 0, 12, 23, 7, 20, 22, 32, 13, 13, 12, 24, 16, 12]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-boot/qualification.json b/test/vm/jsconfuser-vm/fixtures/self-modifying-boot/qualification.json new file mode 100644 index 00000000..d22a1a8b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-boot/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "arithmetic--selfModifying--1", + "case": "arithmetic", + "profile": "selfModifying", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": true, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": { + "kind": "value", + "value": 0 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-boot/source.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-boot/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-boot/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-closures/encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-closures/encoded.js new file mode 100644 index 00000000..92d7c763 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-closures/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */undefined, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */1, /* 4 */3]; +var BYTECODE = [56, 5, 390, 414, 2570702713, 34, 3, 23, 4, 14, 32, 3, 23, 55, 11, 24, 15, 11, 56, 156, 305, 318, 2781205869, 18, 14, 10, 15, 4, 19, 56, 34, 211, 240, 3745043323, 22, 17, 6, 3, 13, 23, 12, 16, 5, 19, 15, 1, 0, 51540, 34, 7, 18, 56, 300, 312, 317, 1751006225, 1, 16, 52073, 20, 11, 2, 10, 45, 4, 56, 70, 240, 245, 4280778561, 34, 15, 15, 4, 22, 56, 80, 381, 390, 1578451386, 4, 11, 56, 299, 192, 200, 3474212842, 4, 10, 45, 4, 12, 24, 16, 12, 4, 10, 0, 4, 0, 0, 45, 4, 56, 154, 257, 266, 1179167123, 56, 263, 151, 161, 730258570, 56, 118, 144, 147, 2081029858, 55, 55, 20, 56, 126, 245, 249, 2512818112, 8, 9, 15, 12, 56, 135, 414, 421, 1735101614, 4, 15, 16, 0, 7, 2, 37, 45, 2, 440511045, 3095212481, 1454562876, 56, 307, 307, 312, 501818177, 16, 5, 19, 6, 27, 21, 0, 4, 56, 322, 278, 290, 1076355419, 23, 19, 5, 2, 56, 261, 200, 205, 3904579366, 19, 16, 5, 9, 34, 6, 23, 32, 8, 1, 56, 367, 255, 259, 1322630380, 16, 10, 17, 8, 56, 157, 320, 333, 2713225533, 55, 14, 21, 16, 8, 18, 0, 23, 13, 17, 16, 3, 1, 2104731282, 463936497, 3118108287, 1477737096, 4132012423, 2491484551, 851227267, 3505530500, 1865376656, 224657289, 2879279767, 1238419089, 3893182916, 2252687739, 611964660, 3266739050, 1625616301, 4279802536, 2639410848, 999262649, 3653435823, 2013198044, 372536154, 3026821077, 1386292095, 4040996602, 2400637261, 759908804, 3414671682, 2639925811, 999796424, 3653968472, 2013701571, 373059138, 872388231, 3526562461, 1886355341, 245629825, 56, 332, 301, 307, 653214154, 17, 3, 6, 1, 25, 5, 24, 17, 36, 16, 23, 21, 22, 20, 23, 56, 304, 369, 374, 4019635200, 32, 16, 23, 56, 340, 296, 303, 1392964937, 16, 13, 1, 20, 36, 4, 7, 36, 3, 16, 4, 8, 18, 18, 10, 24, 15, 3, 10, 6, 56, 260, 307, 314, 819795777, 56, 255, 162, 175, 2826645300, 22, 4, 18, 11, 4, 24, 1, 17, 18115, 36, 13, 23, 56, 301, 294, 305, 1631177435, 35, 2, 24, 55, 5, 9, 15, 12, 17, 3, 7, 19, 13, 1, 4, 35, 3, 16, 55, 56, 305, 271, 282, 2769174992, 27, 0, 21, 22, 56, 323, 167, 177, 359116224, 12, 0, 12, 13, 25, 12, 19, 1, 22, 5, 21, 19, 56, 316, 198, 199, 1445349015, 4, 21, 4232796649, 2592137593, 951741113, 3606421335, 1965624273, 325224273, 2979797301, 1339257147, 3994024236, 930149051, 3584978857, 1944298377, 303507161, 2957685281, 1317330406, 3971565183, 2331101928, 690773909, 3345132439, 1704928909, 64199344, 2718952886, 1077966221, 3732865688, 2092076679, 451425192, 3106248569, 1465213579, 4119395723, 2478986651, 838847125, 3493019317, 1852722104, 94578929, 2749325642, 1108601307, 3763237191, 2122520774, 481854396, 3135911749]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-closures/qualification.json b/test/vm/jsconfuser-vm/fixtures/self-modifying-closures/qualification.json new file mode 100644 index 00000000..eb867134 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-closures/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "closures--selfModifying--1", + "case": "closures", + "profile": "selfModifying", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": true, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-closures/source.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-closures/source.js new file mode 100644 index 00000000..8148a2c5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-closures/source.js @@ -0,0 +1 @@ +function counter(start) { let value = start; return () => ++value; } const next = counter(3); window.TEST_OUTPUT = [next(), next()]; diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-finally/encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-finally/encoded.js new file mode 100644 index 00000000..ddac50ca --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-finally/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */1, /* 1 */undefined, /* 2 */"TEST_OUTPUT", /* 3 */4, /* 4 */0, /* 5 */"window"]; +var BYTECODE = [56, 5, 453, 478, 2570702713, 34, 3, 23, 4, 14, 32, 3, 23, 55, 11, 24, 15, 11, 56, 170, 385, 398, 2781205869, 18, 14, 10, 15, 32, 19, 21, 56, 35, 292, 318, 1684245012, 21, 6, 3, 22, 27, 12, 16, 10, 56, 229, 167, 180, 3377748332, 34, 7, 18, 56, 380, 392, 397, 1751006225, 1, 16, 52073, 4, 11, 45, 4, 15, 22, 24, 15, 56, 72, 478, 480, 2743170804, 4, 14, 59, 102, 3, 4, 136, 0, 5, 0, 0, 11, 6, 0, 5, 5, 4, 6, 1, 3, 143, 55, 39, 102, 55, 1, 3, 145, 39, 102, 56, 107, 450, 453, 1044305650, 55, 4, 11, 56, 115, 398, 405, 3417961225, 24, 22, 10, 4, 32, 13, 9, 56, 127, 365, 374, 4129069473, 16, 12, 1, 10, 56, 192, 379, 388, 974516781, 56, 141, 318, 320, 1179167123, 55, 16, 45, 4, 56, 150, 225, 229, 337466211, 13, 4, 19, 13, 45, 5, 56, 379, 355, 365, 2613096358, 56, 387, 387, 392, 501818177, 16, 5, 19, 6, 27, 21, 0, 4, 56, 233, 349, 361, 1076355419, 23, 19, 5, 2, 56, 332, 214, 219, 3904579366, 19, 16, 5, 9, 34, 6, 23, 32, 8, 1, 56, 278, 326, 330, 1322630380, 16, 10, 17, 8, 56, 171, 231, 244, 2713225533, 55, 14, 21, 16, 8, 18, 0, 23, 13, 17, 16, 3, 1, 2991677192, 1351533312, 4005869375, 2365545728, 1, 17, 18115, 36, 13, 23, 56, 381, 374, 385, 1631177435, 35, 2, 24, 55, 5, 9, 15, 12, 17, 3, 7, 19, 13, 1, 4, 35, 3, 16, 55, 56, 385, 342, 353, 2769174992, 27, 0, 21, 22, 56, 234, 181, 191, 359116224, 12, 0, 12, 13, 25, 12, 19, 1, 22, 5, 21, 19, 56, 396, 212, 213, 1445349015, 4, 21, 43718440, 2698476351, 1057492783, 3712457488, 2071671059, 431008008, 3085066619, 1444800261, 4098978596, 2458574596, 818168583, 3472574731, 1832374591, 191654148, 2846286101, 1205410588, 3860180755, 2219719531, 578848503, 3233745255, 1592947173, 4247775579, 2606474029, 966213436, 3620262714, 1980127022, 3834054392, 2192804136, 56, 243, 381, 387, 653214154, 17, 3, 6, 1, 25, 5, 24, 17, 36, 16, 23, 21, 22, 20, 23, 56, 384, 280, 285, 4019635200, 32, 16, 23, 56, 251, 376, 383, 1392964937, 16, 13, 1, 20, 36, 4, 7, 36, 3, 16, 4, 8, 2488316220, 847911179, 3502604564, 1861875472, 221352947, 2875993983, 1235394797, 3890174303, 2248921379, 18, 18, 10, 24, 15, 3, 10, 6, 56, 331, 387, 394, 819795777, 56, 326, 176, 189, 2826645300, 22, 4, 18, 11, 4, 24, 1777476285, 136882093, 2791658403, 1150680800, 3804775019, 2164442598, 524205691, 13, 1, 15, 16, 55, 5, 15, 24, 15, 19, 12, 0, 27, 1, 8, 20, 13, 18, 20, 15, 56, 381, 380, 391, 1947277152, 19, 19, 20, 2, 17, 8, 21, 12, 12, 20, 14, 21, 56, 415, 168, 180, 1386208053, 34, 9, 12, 3698373603, 2058231648, 417694958, 930149051, 3584978857, 1944298377, 303507161, 2957685283, 1317330406, 3971565170, 2331101929, 690773909, 3345132435, 1704928909, 64199345, 2718952885, 1077966221, 3732865692, 2092076675, 451425158, 3106248572, 1465213580, 4119395720, 2478986651, 838847122, 3493019314, 1852722104, 212129704, 1102773686, 3757479343]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-finally/qualification.json b/test/vm/jsconfuser-vm/fixtures/self-modifying-finally/qualification.json new file mode 100644 index 00000000..76cb365a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-finally/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "tryFinally--selfModifying--1", + "case": "tryFinally", + "profile": "selfModifying", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": true, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-finally/source.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-finally/source.js new file mode 100644 index 00000000..747d80bf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-finally/source.js @@ -0,0 +1 @@ +let count = 0; function check(value) { try { return value + 1; } finally { count++; } } window.TEST_OUTPUT = [check(4), count]; diff --git a/test/vm/jsconfuser-vm/self-modifying-observation.test.js b/test/vm/jsconfuser-vm/self-modifying-observation.test.js new file mode 100644 index 00000000..53c12c5b --- /dev/null +++ b/test/vm/jsconfuser-vm/self-modifying-observation.test.js @@ -0,0 +1,158 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { diagnoseSelfModifyingRegions } from '../../../src/vm/jsconfuser-vm/observe-self-modifying.js' + +const directory = new URL('fixtures/self-modifying-boot/', import.meta.url) +const source = fs.readFileSync(new URL('source.js', directory), 'utf8') +const encoded = fs.readFileSync(new URL('encoded.js', directory), 'utf8') +const closureDirectory = new URL( + 'fixtures/self-modifying-closures/', + import.meta.url, +) +const closureSource = fs.readFileSync( + new URL('source.js', closureDirectory), + 'utf8', +) +const closureEncoded = fs.readFileSync( + new URL('encoded.js', closureDirectory), + 'utf8', +) +const finallyDirectory = new URL( + 'fixtures/self-modifying-finally/', + import.meta.url, +) +const finallySource = fs.readFileSync( + new URL('source.js', finallyDirectory), + 'utf8', +) +const finallyEncoded = fs.readFileSync( + new URL('encoded.js', finallyDirectory), + 'utf8', +) + +function analysesFrom(script) { + const diagnosis = diagnoseContainer(script) + expect(diagnosis.ok).toBe(true) + return diagnosis.result +} + +function modifiedAnalyses(analyses, mutate) { + const changed = structuredClone(analyses) + mutate(changed.roles.words.values) + return changed +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return window.TEST_OUTPUT +} + +test('the emitted first PATCH decrypts a canonical instruction region', () => { + const extracted = diagnoseContainer(encoded) + expect(extracted.ok).toBe(true) + const words = extracted.result.roles.words.values + const [opcode, destPc, sliceStart, sliceEnd, key] = words + expect(opcode).toBe(extracted.result.roles.op.values.PATCH) + expect(destPc).toBe(5) + expect(sliceStart).toBeGreaterThan(destPc) + expect(sliceEnd).toBeLessThanOrEqual(words.length) + expect(sliceEnd - sliceStart).toBe(22) + + let streamKey = (key ^ destPc) | 0 + const clear = words.slice(sliceStart, sliceEnd).map((word) => { + streamKey = (streamKey + 0x9e3779b9) | 0 + return (word ^ (streamKey ^ (streamKey >>> 13))) >>> 0 + }) + const segment = structuredClone(extracted.result) + segment.roles.words.values = clear + expect( + diagnoseWordcode(segment).result.instructions.map(({ name }) => name), + ).toEqual([ + 'LOAD_THIS', + 'LOAD_GLOBAL', + 'LOAD_CONST', + 'LOAD_CONST', + 'LOAD_CONST', + 'MUL', + ]) + expect(words.slice(destPc, destPc + clear.length)).not.toEqual(clear) + expect(diagnoseWordcode(extracted.result)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'unsupported-hardening-opcode' }, + }) + expect(observe(encoded)).toBe(observe(source)) +}) + +test('the sweep materializes every real patch and skips decoys', () => { + for (const [sourceText, encodedText, expectedPatches] of [ + [source, encoded, 2], + [closureSource, closureEncoded, 7], + ]) { + const analyses = analysesFrom(encodedText) + const original = analyses.roles.words.values.slice() + const diagnosis = diagnoseSelfModifyingRegions(analyses) + expect(diagnosis.ok).toBe(true) + const observed = diagnosis.result + expect(observed.patches).toHaveLength(expectedPatches) + expect( + observed.instructions.filter((item) => item.name === 'PATCH'), + ).toHaveLength(expectedPatches) + expect(observed.end).toBeLessThanOrEqual(observed.storageStart) + expect( + observed.patches.every((patch) => patch.destPc === patch.pc + 5), + ).toBe(true) + expect(analyses.roles.words.values).toEqual(original) + expect(observe(encodedText)).toEqual(observe(sourceText)) + } + + const closureAnalyses = analysesFrom(closureEncoded) + const closureObservation = + diagnoseSelfModifyingRegions(closureAnalyses).result + expect( + closureObservation.instructions.filter( + (instruction) => instruction.name === 'MAKE_CLOSURE', + ), + ).toHaveLength(2) + expect( + closureObservation.destinationIntervals.some(({ start, end }) => + closureAnalyses.roles.words.values + .slice(start, end) + .includes(closureAnalyses.roles.op.values.PATCH), + ), + ).toBe(true) +}) + +test('the observer names malformed real-patch intervals and ciphertext', () => { + const analyses = analysesFrom(encoded) + const valid = diagnoseSelfModifyingRegions(analyses) + expect(valid.ok).toBe(true) + const [first, second] = valid.result.patches + + const overlappingSource = modifiedAnalyses(analyses, (words) => { + words[second.pc + 2] = first.sourceStart + words[second.pc + 3] = first.sourceStart + second.width + }) + expect(diagnoseSelfModifyingRegions(overlappingSource)).toMatchObject({ + ok: false, + diagnostic: { code: 'patch-overlap' }, + }) + + const damagedCiphertext = modifiedAnalyses(analyses, (words) => { + let key = (words[first.pc + 4] ^ first.destPc) | 0 + key = (key + 0x9e3779b9) | 0 + words[first.sourceStart] = (0xffffffff ^ (key ^ (key >>> 13))) >>> 0 + }) + expect(diagnoseSelfModifyingRegions(damagedCiphertext)).toMatchObject({ + ok: false, + diagnostic: { code: 'patch-ciphertext' }, + }) +}) From 84cc454c63dc5c03de37fdee6afc8cab6ebea093 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 08/12] feat(vm/jsconfuser-vm): diagnose standalone VM programs Diagnosis runs the frontend in reverse encoder stage order, peeling the outermost stage first, and turns the first refusal into one public decline with the component that refused. It owns the standalone decline contract the decode entry reports. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- src/vm/jsconfuser-vm/diagnose-standalone.js | 385 +++++++ src/vm/jsconfuser-vm/standalone-contract.js | 24 + .../jsconfuser-vm/diagnose-standalone.test.js | 39 + .../encoded-bytecode-arithmetic/encoded.js | 1019 +++++++++++++++++ .../qualification.json | 57 + .../encoded-bytecode-arithmetic/source.js | 1 + 6 files changed, 1525 insertions(+) create mode 100644 src/vm/jsconfuser-vm/diagnose-standalone.js create mode 100644 src/vm/jsconfuser-vm/standalone-contract.js create mode 100644 test/vm/jsconfuser-vm/diagnose-standalone.test.js create mode 100644 test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/source.js diff --git a/src/vm/jsconfuser-vm/diagnose-standalone.js b/src/vm/jsconfuser-vm/diagnose-standalone.js new file mode 100644 index 00000000..881b205d --- /dev/null +++ b/src/vm/jsconfuser-vm/diagnose-standalone.js @@ -0,0 +1,385 @@ +import { + StandaloneDecline, + decline, + requireValue, +} from './standalone-contract.js' +import { extractContainerFromSource } from './extract-container.js' +import { diagnoseWordcode, readWordcode } from './read-wordcode.js' +import { diagnoseNormalizeAntiWordcode } from './normalize-anti-wordcode.js' +import { diagnoseNormalizeAliasedWordcode } from './normalize-aliased-wordcode.js' +import { diagnoseNormalizeMacroWordcode } from './normalize-macro-wordcode.js' +import { composePcMaps } from './expand-observed-word-stream.js' +import { + diagnoseNormalizeAntiSpecializedWordcode, + diagnoseNormalizeSpecializedWordcode, +} from './normalize-specialized-wordcode.js' +import { + diagnoseNormalizeSelfModifyingWordcode, + diagnoseSettleSelfModifyingSuspects, +} from './normalize-self-modifying-wordcode.js' +import { diagnoseNormalizeConcealedConstants } from './normalize-concealed-constants.js' +import { diagnoseNormalizeDispatcherWordcode } from './normalize-dispatcher-wordcode.js' +import { diagnoseNormalizeCffWordcode } from './normalize-cff-wordcode.js' +import { diagnoseNormalizeStringConcealing } from './normalize-string-concealing.js' +import { validateReferences } from './validate-references.js' +import { diagnoseFunctionPartition } from './partition-functions.js' + +const SUPPORTED_INSTRUCTIONS = new Set([ + 'ADD', + 'BAND', + 'BOR', + 'BXOR', + 'CALL', + 'CALL_METHOD', + 'BUILD_ARRAY', + 'BUILD_OBJECT', + 'DEBUGGER', + 'DELETE_PROP', + 'DEFINE_GETTER', + 'DEFINE_SETTER', + 'DIV', + 'EQ', + 'EXP', + 'FINALLY_SETUP', + 'FOR_IN_NEXT', + 'FOR_IN_SETUP', + 'GET_PROP', + 'GTE', + 'GT', + 'IN', + 'INSTANCEOF', + 'JUMP', + 'JUMP_IF_FALSE', + 'JUMP_IF_TRUE', + 'JUMP_REG', + 'LOAD_CONST', + 'LOAD_GLOBAL', + 'LOAD_INT', + 'LOAD_THIS', + 'LOAD_UPVALUE', + 'LOOSE_EQ', + 'LOOSE_NEQ', + 'LT', + 'LTE', + 'MAKE_CLOSURE', + 'MOD', + 'MOVE', + 'MUL', + 'NEW', + 'NEQ', + 'RETURN', + 'SET_PROP', + 'SHL', + 'SHR', + 'STORE_GLOBAL', + 'STORE_UPVALUE', + 'SUB', + 'THROW', + 'TRY_END', + 'TRY_SETUP', + 'TYPEOF', + 'TYPEOF_SAFE', + 'UNARY_BITNOT', + 'UNARY_NEG', + 'UNARY_NOT', + 'UNARY_POS', + 'USHR', + 'VOID', +]) + +function instructionMap(wordcode) { + return new Map( + wordcode.instructions.map((instruction) => [instruction.pc, instruction]), + ) +} + +function functionInstructionMap(wordcode, functions) { + const byPc = instructionMap(wordcode) + return new Map( + functions.functions.map((functionRecord) => [ + functionRecord.id, + functionRecord.instructionPcs.map((pc) => { + const instruction = byPc.get(pc) + return instruction + }), + ]), + ) +} + +// One decline per record builder, keyed by its component: the generic code and the message the +// public diagnostic carries when that builder returns no record. +const DECLINES = Object.freeze({ + 'extract-container': [ + 'container-declined', + 'extractContainerFromSource declined the input container', + ], + 'read-wordcode': [ + 'wordcode-declined', + 'readWordcode declined the input wordcode', + ], + 'validate-references': [ + 'references-declined', + 'validateReferences declined the input references', + ], + 'partition-functions': [ + 'functions-declined', + 'partitionFunctions declined the input function partition', + ], +}) + +function reconstruct(source) { + if (typeof source !== 'string') + decline('invalid-input', 'Standalone decoder input must be a string') + let container = requireValue( + extractContainerFromSource(source), + ...DECLINES['extract-container'], + ) + let records = {} + let wordcode = readWordcode(container) + // Every address the encoder wrote is an origin PC: labels resolve once, after selfModifying, and + // the runtime patches in place. originPcMap (origin PC -> current PC; null while they coincide) + // is recomposed after every stage that moves code, from that stage's own pcMap, and resolves + // those addresses wherever a later stage reads one. + let originPcMap = null + const advance = (result) => { + if (result.pcMap) originPcMap = composePcMaps(originPcMap, result.pcMap) + } + const targetOptions = () => + originPcMap ? { decodedTargetMap: originPcMap, suspects } : {} + let originContinuationLoads = null + // PATCH suspects (origin PCs): fillers selfModifying could not decide while a dispatcher hid + // labels. Each decoder reads past them; once a stage has recovered the labels, settle reruns the + // filler rule and the derived records are rebuilt from the settled stream. + let suspects = [] + let suspectRuns = [] + const recoveredLabels = [] + const settle = () => { + recoveredLabels.push(...(records.skipLabels ?? [])) + if (suspects.length === 0) return false + const settled = diagnoseSettleSelfModifyingSuspects(container, wordcode, { + suspects, + suspectRuns, + originPcMap, + recoveredLabels, + }) + if (!settled.ok) + decline(settled.diagnostic.code, settled.diagnostic.message) + if (!settled.result) return false + container = settled.result.container + wordcode = settled.result.wordcode + advance(settled.result) + suspects = settled.result.suspects + suspectRuns = settled.result.suspectRuns + return true + } + if ( + !wordcode && + ['unknown-opcode', 'unsupported-hardening-opcode'].includes( + diagnoseWordcode(container).diagnostic?.code, + ) + ) { + const patched = diagnoseNormalizeSelfModifyingWordcode(container, { + source, + }) + if (patched.ok) { + container = patched.result.container + wordcode = patched.result.wordcode + originPcMap = patched.result.originPcMap + originContinuationLoads = patched.result.originContinuationLoads + suspects = patched.result.suspects + suspectRuns = patched.result.suspectRuns + } + } + const routeOptions = originPcMap + ? { originPcMap, originContinuationLoads, suspects } + : {} + if ( + !wordcode && + diagnoseWordcode(container).diagnostic?.code === 'unknown-opcode' + ) { + let normalized = diagnoseNormalizeAliasedWordcode( + source, + container, + routeOptions, + ) + if (!normalized.ok) { + normalized = diagnoseNormalizeAliasedWordcode(source, container, { + ...routeOptions, + allowObservedPrefix: true, + }) + } + if (!normalized.ok) { + normalized = diagnoseNormalizeMacroWordcode( + source, + container, + routeOptions, + ) + } + if (!normalized.ok) { + normalized = diagnoseNormalizeSpecializedWordcode( + source, + container, + routeOptions, + ) + } + if (!normalized.ok) { + normalized = diagnoseNormalizeAntiSpecializedWordcode( + source, + container, + routeOptions, + ) + } + if (!normalized.ok) { + normalized = diagnoseNormalizeAntiWordcode( + source, + container, + routeOptions, + ) + } + if (normalized.ok) { + records = normalized.result + container = records.container + wordcode = records.wordcode + // A route publishes origin PCs -> its output; an alias projection keeps PCs and passes the + // incoming map on. + originPcMap = records.originPcMap ?? originPcMap + suspects = records.suspects ?? suspects + if (settle()) records = {} + } + } + wordcode = requireValue(wordcode, ...DECLINES['read-wordcode']) + if ( + wordcode.instructions.some((instruction) => + instruction.operands.some( + (operand) => + operand.kind === 'constant-ref' && operand.concealKey !== 0, + ), + ) + ) { + const diagnosis = diagnoseNormalizeConcealedConstants( + source, + container, + wordcode, + ) + const normalized = requireValue( + diagnosis.ok ? diagnosis.result : null, + 'concealed-constants-declined', + 'Concealed constant normalization declined the input', + ) + container = normalized.container + wordcode = normalized.wordcode + records = normalized + } + let references = requireValue( + records.references ?? validateReferences(container, wordcode), + ...DECLINES['validate-references'], + ) + // Every multi-parent closure entry is published as a suspect (a shared string helper): the + // dispatcher and CFF remap it, the string inverse removes the helper, and ordinary partitionFunctions after the + // string attempt refuses any suspect left. + const partition = () => + requireValue( + diagnoseFunctionPartition(container, wordcode, references, { + sharedParentSuspects: true, + }).result, + ...DECLINES['partition-functions'], + ) + let functions = records.functions ?? partition() + if ( + wordcode.instructions.some((instruction) => instruction.name === 'JUMP_REG') + ) { + const prepared = diagnoseNormalizeDispatcherWordcode( + container, + wordcode, + functions, + targetOptions(), + ) + if (prepared.ok) { + records = prepared.result + advance(records) + container = records.container + wordcode = records.wordcode + references = records.references + functions = records.functions + if (settle()) { + records = {} + references = requireValue( + validateReferences(container, wordcode), + ...DECLINES['validate-references'], + ) + functions = partition() + } + } + } + // CFF reports one of three outcomes: no state loop (the stream carries on), recovered, or a loop + // it read and cannot invert, which is the decode's refusal. A record builder (readWordcode + // through partitionFunctions) refusing CFF's rewritten words is published as that builder's + // refusal. + const cff = diagnoseNormalizeCffWordcode(container, wordcode, functions) + if (cff.ok) { + records = cff.result + advance(records) + container = records.container + wordcode = records.wordcode + references = records.references + functions = records.functions + } else if (cff.diagnostic.code !== 'cff-absent') { + const declined = DECLINES[cff.diagnostic.refusedBy] + if (declined) decline(...declined) + decline( + 'cff-declined', + 'Control flow flattening normalization declined the input', + ) + } + if ( + wordcode.instructions[0]?.name === 'MAKE_CLOSURE' && + wordcode.instructions[1]?.name === 'LOAD_CONST' && + wordcode.instructions[2]?.name === 'CALL' && + wordcode.instructions[0].functionMeta?.paramCount === 1 + ) { + const diagnosis = diagnoseNormalizeStringConcealing( + container, + wordcode, + references, + ) + records = requireValue( + diagnosis.ok ? diagnosis.result : null, + 'string-concealing-declined', + 'String concealment normalization declined the input', + ) + advance(records) + container = records.container + wordcode = records.wordcode + references = records.references + functions = records.functions + } + if (functions.sharedParentStarts.length) + decline(...DECLINES['partition-functions']) + for (const instruction of wordcode.instructions) { + if (!SUPPORTED_INSTRUCTIONS.has(instruction.name)) + decline( + 'unsupported-opcode', + `Standalone emission does not support ${instruction.name}`, + ) + } + return { + constants: references.constants.values, + functions, + instructionsByFunction: functionInstructionMap(wordcode, functions), + wordcode, + } +} + +export function diagnoseStandaloneInput(source) { + try { + return { ok: true, model: reconstruct(source), diagnostic: null } + } catch (error) { + if (error instanceof StandaloneDecline) + return { + ok: false, + model: null, + diagnostic: { code: error.code, message: error.message }, + } + throw error + } +} diff --git a/src/vm/jsconfuser-vm/standalone-contract.js b/src/vm/jsconfuser-vm/standalone-contract.js new file mode 100644 index 00000000..68192556 --- /dev/null +++ b/src/vm/jsconfuser-vm/standalone-contract.js @@ -0,0 +1,24 @@ +export class StandaloneDecline extends Error { + constructor(code, message) { + super(message) + this.name = 'StandaloneDecline' + this.code = code + } +} + +export function decline(code, message) { + throw new StandaloneDecline(code, message) +} + +export function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) + return value + Object.freeze(value) + for (const child of Object.values(value)) deepFreeze(child) + return value +} + +export function requireValue(value, code, message) { + if (!value) decline(code, message) + return value +} diff --git a/test/vm/jsconfuser-vm/diagnose-standalone.test.js b/test/vm/jsconfuser-vm/diagnose-standalone.test.js new file mode 100644 index 00000000..1613d318 --- /dev/null +++ b/test/vm/jsconfuser-vm/diagnose-standalone.test.js @@ -0,0 +1,39 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { diagnoseStandaloneInput } from '../../../src/vm/jsconfuser-vm/diagnose-standalone.js' + +function fixture(relativePath) { + return fs.readFileSync( + new URL(`./fixtures/${relativePath}`, import.meta.url), + 'utf8', + ) +} + +test('standalone diagnosis admits encoded preflight and separates a container decline', () => { + const positive = diagnoseStandaloneInput( + fixture('corpus/raw/f-literals-order/encoded.js'), + ) + expect(positive.ok).toBe(true) + expect(positive.diagnostic).toBeNull() + expect(positive.model.wordcode.instructionCount).toBeGreaterThan(0) + expect(positive.model.instructionsByFunction.size).toBeGreaterThan(0) + expect(positive.model).not.toHaveProperty('output') + + const encoded = diagnoseStandaloneInput( + fixture('encoded-bytecode-arithmetic/encoded.js'), + ) + expect(encoded.ok).toBe(true) + expect(encoded.model.wordcode.instructionCount).toBeGreaterThan(0) + + const negative = diagnoseStandaloneInput( + fixture('corpus/negative/missing-container-role/encoded.js'), + ) + expect(negative).toEqual({ + ok: false, + model: null, + diagnostic: { + code: 'container-declined', + message: 'extractContainerFromSource declined the input container', + }, + }) +}) diff --git a/test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/encoded.js b/test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/encoded.js new file mode 100644 index 00000000..111b55b9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = "BAAAAAEAAAACAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAABAAAAAAAAAAAAAAAEAAAAAgAAAAAAAAAAAAAABQAAAAMAAAAAAAAADQAAAAYAAAAEAAAABQAAAAAAAAAEAAAABAAAAAAAAAALAAAABQAAAAYAAAAEAAAAAAAAAAQAAAAFAAAAAAAAAA8AAAAGAAAABQAAAAQAAAAJAAAAAgAAAAMAAAAGAAAAAAAAAAIAAAAGAAAAAAAAAC0AAAACAAAA"; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = true; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/qualification.json b/test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/qualification.json new file mode 100644 index 00000000..c66ed9a9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "arithmetic--encodeBytecode--147", + "case": "arithmetic", + "profile": "encodeBytecode", + "seed": 147, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": { + "kind": "value", + "value": 0 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/source.js b/test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/encoded-bytecode-arithmetic/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; From 4843ff07b1543b26c0bd9c396e08448d82c3a105 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 09/12] feat(vm/switch): add the shared VM switch backend A VM frontend hands over a typed switch model; the backend needs no encoder knowledge. Its control solver computes which cases run, their handler stacks, edges, indirect targets and reaching definitions from the cases alone; statement planning, copy planning and structured emission turn that into JavaScript, with dispatch kept as the review rendering. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- src/vm/switch/vm-switch-control.js | 506 +++++++ src/vm/switch/vm-switch-copy-planning.js | 510 +++++++ src/vm/switch/vm-switch-model.js | 16 + src/vm/switch/vm-switch-statements.js | 1321 +++++++++++++++++ src/vm/switch/vm-switch-structured-emitter.js | 1321 +++++++++++++++++ src/vm/switch/vm-switch-to-source.js | 227 +++ test/vm/switch/vm-switch-to-source.test.js | 305 ++++ 7 files changed, 4206 insertions(+) create mode 100644 src/vm/switch/vm-switch-control.js create mode 100644 src/vm/switch/vm-switch-copy-planning.js create mode 100644 src/vm/switch/vm-switch-model.js create mode 100644 src/vm/switch/vm-switch-statements.js create mode 100644 src/vm/switch/vm-switch-structured-emitter.js create mode 100644 src/vm/switch/vm-switch-to-source.js create mode 100644 test/vm/switch/vm-switch-to-source.test.js diff --git a/src/vm/switch/vm-switch-control.js b/src/vm/switch/vm-switch-control.js new file mode 100644 index 00000000..27306bed --- /dev/null +++ b/src/vm/switch/vm-switch-control.js @@ -0,0 +1,506 @@ +// Control over one function's typed cases: which cases run, the handler stack each one runs under, +// the edges between them, the finite targets of each indirect jump and the register definitions +// reaching each case. Reachability and handler-state flow need no knowledge of the encoder: it +// reads only the typed operations and the frontend's facts, so every VM frontend shares it. +import { VmSwitchDecline } from './vm-switch-model.js' + +const MAX_ROUNDS = 32 + +function fail(code, message) { + throw new VmSwitchDecline(code, message) +} + +// The typed operations whose JavaScript can complete abruptly. A strict comparison, `!`, typeof +// of a register and the moves between registers, captures and fresh arrays or closures cannot; +// a global read through an accessor, a coercing operator or a property operation can. +function mayThrow(operation) { + switch (operation.kind) { + case 'call': + case 'throw': + case 'global-read': + case 'global-write': + case 'safe-typeof': + case 'property-read': + case 'property-write': + case 'property-delete': + case 'object': + case 'accessor': + case 'for-in-setup': + return true + case 'assign': + if (operation.value.kind === 'binary') + return !['===', '!=='].includes(operation.value.operator) + if (operation.value.kind === 'unary') + return operation.value.operator !== '!' + return false + default: + return false + } +} + +function makeRecords(cases) { + const records = new Map() + for (const { pc, operation } of cases) { + if (operation.kind === 'catch-setup') + records.set(pc, { + kind: 'catch', + setupPc: pc, + target: operation.target, + exceptionRegister: operation.exceptionRegister, + }) + else if (operation.kind === 'finally-setup') + records.set(pc, { + kind: 'finally', + setupPc: pc, + target: operation.target, + continuation: operation.continuation, + payload: operation.payload, + throwPad: operation.throwPad, + }) + } + return records +} + +function sameStack(left, right) { + return ( + left.length === right.length && + left.every((record, index) => record === right[index]) + ) +} + +function edge(kind, sourcePc, targetPc, options = {}) { + return { + kind, + sourcePc, + targetPc, + route: options.route ?? 'direct', + mode: options.mode ?? 'direct', + condition: options.condition ?? null, + callSitePc: options.callSitePc ?? null, + continuationPc: options.continuationPc ?? null, + writes: options.writes ?? [], + } +} + +// An abrupt completion at `pc` goes to the innermost active record, which it pops: a catch +// receives the exception in its register, a finalizer resumes at its throw pad with the exception +// as payload. With nothing active it leaves the function. +function abruptRoute(pc, stack, callSitePc) { + const top = stack.at(-1) + if (!top) + return { + edge: edge('throw', pc, null, { route: 'propagate', callSitePc }), + after: [], + } + const after = stack.slice(0, -1) + if (top.kind === 'catch') + return { + edge: edge('handler', pc, top.target, { + route: 'exceptional', + callSitePc, + writes: [{ register: top.exceptionRegister, kind: 'unknown' }], + }), + after, + } + return { + edge: edge('finally', pc, top.target, { + route: 'exceptional', + callSitePc, + writes: [ + { + register: top.continuation, + kind: 'literal', + value: top.throwPad, + origin: `exception@${pc}`, + }, + { register: top.payload, kind: 'unknown' }, + ], + }), + after, + } +} + +function successors(item, stack, context) { + const { pc, operation } = item + const { casesByPc, records, finallyTargets, jumpTargets, frame } = context + const next = casesByPc.has(item.nextPc) ? item.nextPc : null + const out = [] + const fallthrough = (after = stack) => { + if (next !== null) out.push({ edge: edge('fallthrough', pc, next), after }) + else if (after.length) + fail( + 'unbalanced-handler-stack', + `Function ${frame.id} ends with an active handler at ${pc}`, + ) + } + switch (operation.kind) { + case 'catch-setup': + case 'finally-setup': { + const active = [...stack, records.get(pc)] + fallthrough(active) + // Whether a try body throws cannot be decided (a call into imported code, an accessor), so + // the handler may run whenever its try is entered: the setup routes to it. + out.push(abruptRoute(pc, active, null)) + break + } + case 'handler-pop': + if (!stack.length) + fail( + 'handler-stack-underflow', + `Function ${frame.id}: handler-pop@${pc} has nothing active`, + ) + fallthrough(stack.slice(0, -1)) + break + case 'jump': + out.push({ + edge: finallyTargets.has(operation.target) + ? edge('finally', pc, operation.target, { route: 'normal' }) + : edge('branch', pc, operation.target), + after: stack, + }) + break + case 'branch': + out.push({ + edge: edge('conditional', pc, operation.target, { + condition: { + register: operation.condition, + truthy: operation.when === 'true', + }, + }), + after: stack, + }) + fallthrough() + break + case 'for-in-next': + out.push({ + edge: edge('branch', pc, operation.exit, { + condition: { + kind: 'iterator', + register: operation.iterator, + available: false, + }, + }), + after: stack, + }) + fallthrough() + break + case 'indirect-jump': + for (const target of jumpTargets.get(pc) ?? []) + out.push({ + edge: edge('branch', pc, target, { mode: 'indirect' }), + after: stack, + }) + break + case 'call': + if (next === null) + fail( + 'missing-call-continuation', + `Function ${frame.id}: call@${pc} has no continuation`, + ) + out.push({ + edge: edge('call', pc, next, { + route: 'normal', + callSitePc: pc, + continuationPc: next, + }), + after: stack, + }) + out.push(abruptRoute(pc, stack, pc)) + break + case 'throw': + out.push(abruptRoute(pc, stack, null)) + break + // The runtime's return closes the frame without running a finalizer; the compiler routes a + // return out of a protected region through the finalizer explicitly. + case 'return': + out.push({ + edge: edge('return', pc, null, { route: 'propagate' }), + after: [], + }) + break + default: + fallthrough() + if (stack.length && mayThrow(operation)) + out.push(abruptRoute(pc, stack, null)) + } + return out +} + +function solveStacks(cases, context) { + const startPc = context.frame.startPc + const stateIn = new Map([[startPc, []]]) + const queue = [startPc] + const edges = [] + while (queue.length) { + const pc = queue.shift() + const before = stateIn.get(pc) + for (const { edge: item, after } of successors( + context.casesByPc.get(pc), + before, + context, + )) { + edges.push({ ...item, stackBefore: before, stackAfter: after }) + if (item.targetPc === null) continue + const prior = stateIn.get(item.targetPc) + if (!prior) { + stateIn.set(item.targetPc, after) + queue.push(item.targetPc) + } else if (!sameStack(prior, after)) + fail( + 'handler-stack-merge-disagreement', + `Function ${context.frame.id}: incompatible handler stacks at ${item.targetPc}`, + ) + } + } + return { stateIn, edges } +} + +// A definition is one write of a register: a case's destination, a route's write on its edge or +// the register's value at function entry. Each case's in-state names, per register, the +// definitions reaching it; a definition's value is a literal, a copy of the definitions reaching +// its source, or unknown. +function definitionOf(item) { + const { operation } = item + if (operation.destination === undefined) return null + const value = + operation.kind === 'assign' && operation.value.kind === 'literal' + ? { kind: 'literal', value: operation.value.value } + : operation.kind === 'assign' && operation.value.kind === 'register' + ? { kind: 'copy', source: operation.value.index } + : { kind: 'unknown' } + return { + id: String(item.pc), + pc: item.pc, + register: operation.destination, + value, + origin: item.pc, + } +} + +function solveDefinitions(stacks, context) { + const { frame, casesByPc } = context + const definitions = new Map() + const entry = Array.from({ length: frame.regCount }, (_, register) => { + const id = `entry:${register}` + definitions.set(id, { + id, + pc: null, + register, + value: { kind: 'unknown' }, + origin: null, + }) + return new Set([id]) + }) + const caseDefinitions = new Map() + for (const pc of stacks.stateIn.keys()) { + const definition = definitionOf(casesByPc.get(pc)) + if (definition) { + definitions.set(definition.id, definition) + caseDefinitions.set(pc, definition) + } + } + const edgesBySource = new Map() + for (const item of stacks.edges) { + if (item.targetPc === null) continue + const list = edgesBySource.get(item.sourcePc) ?? [] + list.push(item) + edgesBySource.set(item.sourcePc, list) + item.writes.forEach((write, index) => { + const id = `route@${item.sourcePc}:${item.kind}:${index}` + definitions.set(id, { + id, + pc: item.sourcePc, + register: write.register, + value: + write.kind === 'literal' + ? { kind: 'literal', value: write.value } + : { kind: 'unknown' }, + origin: write.origin ?? null, + }) + write.definition = id + }) + } + + const reaching = new Map([[frame.startPc, entry]]) + const queue = [frame.startPc] + while (queue.length) { + const pc = queue.shift() + const output = reaching.get(pc).map((set) => new Set(set)) + const own = caseDefinitions.get(pc) + // A for-in advance writes its key only when one is available: on its fallthrough, not its exit. + const ownOnFallthrough = + own && casesByPc.get(pc).operation.kind === 'for-in-next' + if (own && !ownOnFallthrough) output[own.register] = new Set([own.id]) + for (const item of edgesBySource.get(pc) ?? []) { + const state = output.map((set) => new Set(set)) + if (ownOnFallthrough && item.kind === 'fallthrough') + state[own.register] = new Set([own.id]) + for (const write of item.writes) + state[write.register] = new Set([write.definition]) + const prior = reaching.get(item.targetPc) + if (!prior) { + reaching.set(item.targetPc, state) + queue.push(item.targetPc) + continue + } + let changed = false + prior.forEach((set, register) => { + for (const id of state[register]) + if (!set.has(id)) { + set.add(id) + changed = true + } + }) + if (changed) queue.push(item.targetPc) + } + } + return { definitions, reaching } +} + +// The values a definition can hold, each with the origin PCs that wrote it; a copy holds what the +// definitions reaching its source hold, solved to a fixed point over copy cycles. +function solveDefinitionValues({ definitions, reaching }) { + const values = new Map() + for (const definition of definitions.values()) + values.set(definition.id, { + unknown: definition.value.kind === 'unknown', + values: new Map( + definition.value.kind === 'literal' + ? [[definition.value.value, new Set([String(definition.origin)])]] + : [], + ), + }) + const copies = [...definitions.values()].filter( + ({ value }) => value.kind === 'copy', + ) + let changed = true + while (changed) { + changed = false + for (const copy of copies) { + const cell = values.get(copy.id) + for (const id of reaching.get(copy.pc)[copy.value.source]) { + const source = values.get(id) + if (source.unknown && !cell.unknown) { + cell.unknown = true + changed = true + } + for (const [value, origins] of source.values) { + const known = cell.values.get(value) ?? new Set() + for (const origin of origins) + if (!known.has(origin)) { + known.add(origin) + changed = true + } + cell.values.set(value, known) + } + } + } + } + return values +} + +function indirectTargets(stacks, flow, values, context) { + const { frame, casesByPc } = context + const jumps = [] + for (const pc of stacks.stateIn.keys()) { + const { operation } = casesByPc.get(pc) + if (operation.kind !== 'indirect-jump') continue + const origins = new Map() + let unknown = false + for (const id of flow.reaching.get(pc)[operation.source]) { + const cell = values.get(id) + unknown = unknown || cell.unknown + for (const [value, from] of cell.values) { + const known = origins.get(value) ?? new Set() + for (const origin of from) known.add(origin) + origins.set(value, known) + } + } + if (unknown || origins.size === 0) + fail( + 'unresolved-indirect-jump', + `Function ${frame.id}: indirect-jump@${pc} has no finite target set`, + ) + const targets = [...origins.keys()] + for (const target of targets) + if (!casesByPc.has(target)) + fail( + 'indirect-target-outside-function', + `Function ${frame.id}: indirect-jump@${pc} targets ${String(target)}, not a case of the function`, + ) + targets.sort((left, right) => left - right) + jumps.push({ + pc, + sourceRegister: operation.source, + targets, + origins: Object.fromEntries( + targets.map((target) => [target, [...origins.get(target)].sort()]), + ), + }) + } + return jumps.sort((left, right) => left.pc - right.pc) +} + +function sameTargets(left, right) { + if (left.size !== right.size) return false + for (const [pc, targets] of left) { + const other = right.get(pc) + if ( + !other || + other.length !== targets.length || + other.some((target, index) => target !== targets[index]) + ) + return false + } + return true +} + +// Indirect targets feed the edges the stack solve follows, and the edges feed the definitions +// that give the targets, so the two solve together until the targets stop changing. +export function solveControl(frame, cases) { + const casesByPc = new Map(cases.map((item) => [item.pc, item])) + const records = makeRecords(cases) + const context = { + frame, + casesByPc, + records, + finallyTargets: new Set( + [...records.values()] + .filter(({ kind }) => kind === 'finally') + .map(({ target }) => target), + ), + jumpTargets: new Map( + cases + .filter(({ operation }) => operation.kind === 'indirect-jump') + .map(({ pc }) => [pc, []]), + ), + } + for (let round = 0; round < MAX_ROUNDS; round += 1) { + const stacks = solveStacks(cases, context) + const flow = solveDefinitions(stacks, context) + const values = solveDefinitionValues(flow) + const jumps = indirectTargets(stacks, flow, values, context) + const next = new Map(jumps.map(({ pc, targets }) => [pc, targets])) + for (const pc of context.jumpTargets.keys()) + if (!next.has(pc)) next.set(pc, []) + if (sameTargets(context.jumpTargets, next)) { + const reachablePcs = [...stacks.stateIn.keys()].sort( + (left, right) => left - right, + ) + return { + functionId: frame.id, + reachablePcs, + stateIn: stacks.stateIn, + edges: stacks.edges, + indirectTargets: jumps, + definitions: flow.definitions, + reachingDefinitions: flow.reaching, + } + } + context.jumpTargets = next + } + fail( + 'control-nonconvergence', + `Function ${frame.id}: indirect targets did not converge`, + ) +} diff --git a/src/vm/switch/vm-switch-copy-planning.js b/src/vm/switch/vm-switch-copy-planning.js new file mode 100644 index 00000000..be02701c --- /dev/null +++ b/src/vm/switch/vm-switch-copy-planning.js @@ -0,0 +1,510 @@ +import { VmSwitchDecline, isJumpStep } from './vm-switch-model.js' + +export function decline(code, message) { + throw new VmSwitchDecline(code, message) +} + +export function requireValue(value, code, message) { + if (!value) decline(code, message) + return value +} + +function literal(value) { + if (value === undefined) return 'void 0' + if (value === null) return 'null' + if (typeof value === 'string') return JSON.stringify(value) + if (typeof value === 'boolean') return value ? 'true' : 'false' + if (typeof value === 'number') { + if (Number.isNaN(value)) return 'NaN' + if (value === Infinity) return 'Infinity' + if (value === -Infinity) return '-Infinity' + if (Object.is(value, -0)) return '-0' + return String(value) + } + if (typeof value === 'bigint') return `${String(value)}n` + decline('unsupported-constant-value', 'Constant is not a supported literal') +} + +export function functionName(functionId) { + return `__recovered_function_${functionId}` +} + +export function registerName(functionId, register) { + return `__recovered_r_${functionId}_${register}` +} + +export function functionRegisterSet(instructions) { + const registers = new Set() + for (const instruction of instructions) { + for (const register of readRegisters(instruction)) registers.add(register) + const destination = destinationRegister(instruction) + if (destination !== null) registers.add(destination) + const operation = instruction.operation + if (operation.kind === 'catch-setup') + registers.add(operation.exceptionRegister) + if (operation.kind === 'finally-setup') { + registers.add(operation.continuation) + registers.add(operation.payload) + } + if (operation.kind === 'closure') + for (const capture of operation.captures) + if (capture.kind === 'local') registers.add(capture.index) + } + return registers +} + +export function localCaptureRegisterSet(instructions) { + const registers = new Set() + for (const instruction of instructions) { + if (instruction.operation.kind !== 'closure') continue + for (const capture of instruction.operation.captures) + if (capture.kind === 'local') registers.add(capture.index) + } + return registers +} + +export function functionNeedsArguments(instructions, functionRecord) { + const reads = registerReadCounts(instructions) + const usedRegisters = new Set([ + ...reads.keys(), + ...localCaptureRegisterSet(instructions), + ]) + const argumentRegisters = [] + for (let register = 0; register < functionRecord.paramCount; register += 1) + argumentRegisters.push(register) + if (functionRecord.paramCount < functionRecord.regCount) + argumentRegisters.push(functionRecord.paramCount) + return argumentRegisters.some((register) => usedRegisters.has(register)) +} + +function readRegisters(instruction) { + const op = instruction.operation + if (op.kind === 'assign') { + const value = op.value + if (value.kind === 'register') return [value.index] + if (value.kind === 'binary') return [value.left, value.right] + if (value.kind === 'unary' || value.kind === 'typeof') return [value.source] + return [] + } + if (op.kind === 'global-write' || op.kind === 'upvalue-write') + return [op.source] + if (op.kind === 'property-read' || op.kind === 'property-delete') + return [op.object, op.key] + if (op.kind === 'property-write') return [op.object, op.key, op.value] + if (op.kind === 'branch') return [op.condition] + if (['indirect-jump', 'return', 'throw', 'for-in-setup'].includes(op.kind)) + return [op.source] + if (op.kind === 'call') + return [ + ...(op.receiver.kind === 'register' ? [op.receiver.index] : []), + op.callee, + ...op.arguments.registers, + ] + if (op.kind === 'array') return op.elements + if (op.kind === 'object') + return op.pairs.flatMap(({ key, value }) => [key, value]) + if (op.kind === 'accessor') return [op.object, op.key, op.callback] + if (op.kind === 'for-in-next') return [op.iterator] + return [] +} + +function registerReadCounts(instructions) { + const counts = new Map() + for (const instruction of instructions) { + for (const register of readRegisters(instruction)) + counts.set(register, (counts.get(register) || 0) + 1) + } + return counts +} + +function destinationRegister(instruction) { + // Iterator advance writes only on its non-exit route; adjacent-copy elimination + // cannot redirect it as an unconditional definition. + if (instruction.operation.kind === 'for-in-next') return null + return Number.isSafeInteger(instruction.operation.destination) + ? instruction.operation.destination + : null +} + +// Every read of a reachable case, keyed by each definition reaching it at that case (the control +// solver's reaching definitions); one read of a register reached by two definitions is a use of each. +function reachingDefinitionUses(instructions, reachingDefinitions) { + const uses = new Map() + for (const instruction of instructions) { + const state = reachingDefinitions.get(instruction.pc) + if (!state) continue + for (const register of readRegisters(instruction)) { + for (const definition of state[register]) { + const sites = uses.get(definition) || [] + sites.push({ pc: instruction.pc, register }) + uses.set(definition, sites) + } + } + } + return uses +} + +function adjacentCopyPlan(functionId, instructions, solved) { + const reachablePcs = new Set(solved.reachablePcs) + const uses = reachingDefinitionUses(instructions, solved.reachingDefinitions) + // A closure reads and writes a locally captured register through its cell at any call, which no + // reaching definition records. + const localCaptureRegisters = localCaptureRegisterSet(instructions) + const plan = new Map() + const links = [] + const coalescedMovePcs = new Set() + // A coalesced move's value is written by the producer it was folded into; following these + // links from any removed move reaches the retained producer of its chain. + const foldedInto = new Map() + const retainedProducer = (pc) => { + while (foldedInto.has(pc)) pc = foldedInto.get(pc) + return pc + } + // A node is one case; its successors are the control solver's edges from its PC. + const nodes = [...instructions] + .sort((left, right) => left.pc - right.pc) + .map((instruction) => ({ startPc: instruction.pc, instruction })) + const nodeByPc = new Map(nodes.map((node) => [node.startPc, node])) + const outgoing = Map.groupBy(solved.edges, (edge) => edge.sourcePc) + const incoming = new Map() + for (const edge of solved.edges) + if (edge.targetPc !== null) + incoming.set(edge.targetPc, (incoming.get(edge.targetPc) ?? 0) + 1) + const nodeAt = (pc) => (pc === null ? null : (nodeByPc.get(pc) ?? null)) + + const consider = (producer, move) => { + if ( + !reachablePcs.has(producer.pc) || + !reachablePcs.has(move.pc) || + move.operation.kind !== 'assign' || + move.operation.value.kind !== 'register' || + destinationRegister(producer) === null + ) + return + const producerDestination = destinationRegister(producer) + const moveDestination = destinationRegister(move) + const moveSource = move.operation.value.index + if ( + producerDestination !== moveSource || + producerDestination === moveDestination || + localCaptureRegisters.has(producerDestination) || + localCaptureRegisters.has(moveDestination) + ) + return + const sites = uses.get(`${producer.pc}`) || [] + if (sites.length !== 1 || sites[0].pc !== move.pc) return + if (plan.has(producer.pc) || coalescedMovePcs.has(move.pc)) return + // Earlier coalesces may already have changed both instructions: a removed producer writes + // through its chain's retained producer, and a move already planned as a producer hands its + // retarget on, so the retained producer writes the register at the end of the chain. + const retained = retainedProducer(producer.pc) + plan.set(retained, plan.get(move.pc) ?? moveDestination) + plan.delete(move.pc) + foldedInto.set(move.pc, retained) + coalescedMovePcs.add(move.pc) + links.push({ producerPc: producer.pc, movePc: move.pc, moveSource }) + } + + for (const node of nodes) { + for (const edge of outgoing.get(node.startPc) ?? []) { + if (edge.kind !== 'fallthrough') continue + let target = nodeAt(edge.targetPc) + // Jump steps entered only from here run nothing between the two. + const seen = new Set() + while ( + isJumpStep(target) && + incoming.get(target.startPc) === 1 && + !seen.has(target.startPc) + ) { + seen.add(target.startPc) + const [next] = outgoing.get(target.startPc) ?? [] + target = nodeAt(next?.targetPc ?? null) + } + if (target && !isJumpStep(target) && incoming.get(target.startPc) === 1) + consider(node.instruction, target.instruction) + } + } + // Each row reports a removed move against the final plan: the producer that is retained and + // the register it now writes; the reaching definition is the move's own producer. + const proofs = links.map(({ producerPc, movePc, moveSource }) => { + const retained = retainedProducer(movePc) + return { + functionId, + producerPc: retained, + movePc, + sourceRegister: moveSource, + destinationRegister: plan.get(retained), + reachingDefinitions: [`${producerPc}`], + useSites: [{ pc: movePc, register: moveSource }], + disposition: 'safely-eliminated', + reason: 'single-reaching-definition-use', + producerRetained: true, + evaluationOrderPreserved: true, + receiverSemanticsPreserved: true, + } + }) + return { plan, proofs, coalescedMovePcs } +} + +export function effectiveStructuredInstructions( + functionId, + instructions, + solved, +) { + const adjacent = adjacentCopyPlan(functionId, instructions, solved) + const { plan: copyPlan, proofs: copyProofs, coalescedMovePcs } = adjacent + return { + copyPlan, + copyProofs, + coalescedMovePcs, + instructions: instructions + .filter(({ pc }) => !coalescedMovePcs.has(pc)) + .map((instruction) => { + const destination = copyPlan.get(instruction.pc) + return destination === undefined + ? instruction + : retargetDestination(instruction, destination) + }), + } +} + +export function retargetDestination(instruction, register) { + return { + ...instruction, + operation: { ...instruction.operation, destination: register }, + } +} + +function functionForPc(functions, startPc) { + const functionRecord = functions.functions.find( + ({ startPc: candidate }) => candidate === startPc, + ) + if (!functionRecord) { + decline( + 'missing-closure-function', + `Closure points at missing function entry ${startPc}`, + ) + } + return functionRecord +} + +function captureExpression(functionId, instruction, functions) { + const { captures: captureRecords, entry } = instruction.operation + const captures = [] + for (const capture of captureRecords) { + if (!capture || !Number.isSafeInteger(capture.index) || capture.index < 0) { + decline( + 'invalid-capture-pair', + 'Closure contains an invalid capture pair', + ) + } + if (capture.kind === 'local') { + const cell = `__recovered_cells_${functionId}[${capture.index}]` + captures.push(cell) + } else if (capture.kind === 'upvalue') { + captures.push(`__recovered_captures[${capture.index}]`) + } else { + decline('unsupported-capture-kind', 'Closure has an unknown capture kind') + } + } + functionForPc(functions, entry) + return captures +} + +function argExpression(functionId, args) { + if (args.kind === 'array-register') + return registerName(functionId, args.registers[0]) + return `[${args.registers.map((register) => registerName(functionId, register)).join(', ')}]` +} + +function emitCall(functionId, operation) { + const destination = registerName(functionId, operation.destination) + const callee = registerName(functionId, operation.callee) + const args = argExpression(functionId, operation.arguments) + if (operation.mode === 'apply' && operation.receiver.kind === 'none') { + const { kind, registers } = operation.arguments + const list = + kind === 'array-register' + ? `...${registerName(functionId, registers[0])}` + : registers + .map((register) => registerName(functionId, register)) + .join(', ') + return `${destination} = ${callee}(${list});` + } + if (operation.mode === 'apply') { + const receiver = registerName(functionId, operation.receiver.index) + return `${destination} = Reflect.apply(${callee}, ${receiver}, ${args});` + } + return `${destination} = Reflect.construct(${callee}, ${args});` +} + +function emitCollection(functionId, operation) { + const destination = registerName(functionId, operation.destination) + if (operation.kind === 'array') { + const values = operation.elements.map((register) => + registerName(functionId, register), + ) + return `${destination} = [${values.join(', ')}];` + } + const lines = [`${destination} = {};`] + for (const pair of operation.pairs) { + const key = registerName(functionId, pair.key) + const value = registerName(functionId, pair.value) + lines.push(`Reflect.set(${destination}, ${key}, ${value});`) + } + return lines.join('\n') +} + +export function emitInstruction( + functionId, + instruction, + functions, + emissionMode = 'dispatch', +) { + const operation = instruction.operation + if (!operation) + decline( + 'missing-switch-operation', + 'VM switch case has no semantic operation', + ) + const nextPc = instruction.nextPc + const next = + emissionMode === 'dispatch' + ? `__recovered_pc = ${nextPc};\ncontinue __recovered_dispatch;` + : '' + const destination = () => registerName(functionId, operation.destination) + const source = (register) => registerName(functionId, register) + + if (operation.kind === 'assign') { + const value = operation.value + let expression + if (value.kind === 'literal') expression = literal(value.value) + else if (value.kind === 'this') expression = 'this' + else if (value.kind === 'register') expression = source(value.index) + else if (value.kind === 'binary') + expression = `${source(value.left)} ${value.operator} ${source(value.right)}` + else if (value.kind === 'unary') + expression = `${value.operator}${source(value.source)}` + else if (value.kind === 'typeof') + expression = `typeof ${source(value.source)}` + else if (value.kind === 'undefined') expression = 'void 0' + else + decline( + 'unsupported-switch-value', + `Unknown VM switch value ${value.kind}`, + ) + return `${destination()} = ${expression};\n${next}` + } + if (operation.kind === 'global-read') { + const value = literal(operation.name) + return `if (!(${value} in globalThis)) throw new ReferenceError(${value} + ' is not defined');\n${destination()} = globalThis[${value}];\n${next}` + } + if (operation.kind === 'global-write') + return `globalThis[${literal(operation.name)}] = ${source(operation.source)};\n${next}` + if (operation.kind === 'upvalue-read') + return `${destination()} = __recovered_captures[${operation.index}].value;\n${next}` + if (operation.kind === 'upvalue-write') + return `__recovered_captures[${operation.index}].value = ${source(operation.source)};\n${next}` + if (operation.kind === 'property-read') + return `${destination()} = ${source(operation.object)}[${source(operation.key)}];\n${next}` + if (operation.kind === 'property-write') + return `Reflect.set(${source(operation.object)}, ${source(operation.key)}, ${source(operation.value)});\n${next}` + if (operation.kind === 'property-delete') + return `${destination()} = Reflect.deleteProperty(${source(operation.object)}, ${source(operation.key)});\n${next}` + if (operation.kind === 'safe-typeof') { + const global = literal(operation.name) + return `${destination()} = Object.prototype.hasOwnProperty.call(globalThis, ${global}) ? typeof globalThis[${global}] : 'undefined';\n${next}` + } + if (operation.kind === 'jump') + return `__recovered_pc = ${operation.target};\ncontinue __recovered_dispatch;` + if (operation.kind === 'branch') { + const condition = source(operation.condition) + const predicate = operation.when === 'false' ? `!${condition}` : condition + return `__recovered_pc = ${predicate} ? ${operation.target} : ${nextPc};\ncontinue __recovered_dispatch;` + } + if (operation.kind === 'indirect-jump') + return `__recovered_pc = ${source(operation.source)};\ncontinue __recovered_dispatch;` + if (operation.kind === 'call') + return `${emitCall(functionId, operation)}\n${next}` + if (operation.kind === 'return') return `return ${source(operation.source)};` + if (operation.kind === 'throw') return `throw ${source(operation.source)};` + if (operation.kind === 'closure') { + const captures = captureExpression(functionId, instruction, functions) + const child = functionForPc(functions, operation.entry) + const captureName = `__recovered_closure_captures_${functionId}_${instruction.pc}` + const localCells = operation.captures + .filter((operand) => operand.kind === 'local') + .map((operand) => operand.index) + const statements = localCells.map( + (register) => + `__recovered_cells_${functionId}[${register}] ||= { get value() { return ${registerName(functionId, register)}; }, set value(__value) { ${registerName(functionId, register)} = __value; } };`, + ) + if (child.captureCount > 0) + statements.push(`const ${captureName} = [${captures.join(', ')}];`) + const childArguments = + child.captureCount > 0 + ? `[${captureName}, ...__recovered_closure_args]` + : '[...__recovered_closure_args]' + statements.push( + `${destination()} = function (...__recovered_closure_args) { const __recovered_this = this == null ? globalThis : this; return Reflect.apply(${functionName(child.id)}, __recovered_this, ${childArguments}); };`, + ) + statements.push(next) + return statements.join('\n') + } + if (operation.kind === 'array' || operation.kind === 'object') + return `${emitCollection(functionId, operation)}\n${next}` + if (operation.kind === 'accessor') { + const object = source(operation.object) + const key = source(operation.key) + const callback = source(operation.callback) + const descriptorName = `__recovered_descriptor_${functionId}_${instruction.pc}` + const existingName = `__recovered_existing_${functionId}_${instruction.pc}` + const accessor = operation.accessor + const other = accessor === 'get' ? 'set' : 'get' + return `{\nconst ${existingName} = Object.getOwnPropertyDescriptor(${object}, ${key});\nconst ${descriptorName} = { ${accessor}: ${callback}, configurable: true, enumerable: true };\nif (${existingName} && typeof ${existingName}.${other} === 'function') ${descriptorName}.${other} = ${existingName}.${other};\nObject.defineProperty(${object}, ${key}, ${descriptorName});\n}\n${next}` + } + if (operation.kind === 'for-in-setup') + return `${destination()} = { keys: __recovered_enumerable_keys(${source(operation.source)}), index: 0 };\n${next}` + if (operation.kind === 'for-in-next') { + const iterator = source(operation.iterator) + return `if (${iterator}.index >= ${iterator}.keys.length) { __recovered_pc = ${operation.exit}; } else { ${destination()} = ${iterator}.keys[${iterator}.index++]; __recovered_pc = ${nextPc}; }\ncontinue __recovered_dispatch;` + } + if (operation.kind === 'catch-setup') + return `__recovered_handlers.push({ kind: 'catch', target: ${operation.target}, register: ${operation.exceptionRegister} });\n${next}` + if (operation.kind === 'handler-pop') + return `__recovered_handlers.pop();\n${next}` + if (operation.kind === 'finally-setup') + return `__recovered_handlers.push({ kind: 'finally', target: ${operation.target}, continuation: ${operation.continuation}, payload: ${operation.payload}, throwPad: ${operation.throwPad} });\n${next}` + if (operation.kind === 'debugger') return next + decline( + 'unsupported-switch-operation', + `VM switch emission does not support ${operation.kind}`, + ) +} + +export function emitHandlerRegisterAssignment( + functionId, + registerCount, + property, + value, +) { + const lines = [`switch (__recovered_handler.${property}) {`] + for (let register = 0; register < registerCount; register += 1) { + lines.push( + `case ${register}: ${registerName(functionId, register)} = ${value}; break;`, + ) + } + lines.push("default: throw new Error('Invalid recovered handler register');") + lines.push('}') + return lines +} + +export function appendRendered(lines, rendered) { + const renderedLines = rendered.split('\n') + while (renderedLines.at(-1) === '') renderedLines.pop() + lines.push(...renderedLines) +} + +export function invertPredicate(predicate) { + return predicate.startsWith('!') ? predicate.slice(1) : `!(${predicate})` +} diff --git a/src/vm/switch/vm-switch-model.js b/src/vm/switch/vm-switch-model.js new file mode 100644 index 00000000..2d461ef4 --- /dev/null +++ b/src/vm/switch/vm-switch-model.js @@ -0,0 +1,16 @@ +export const VM_SWITCH_SCHEMA = 'decode-js-vm-switch.v1' + +// A node (one case) whose operation is an unconditional jump is a step of straight-line flow: it +// runs nothing, so a reader that follows control or compares where flow arrives looks through it +// whether the producer laid the target out next or elsewhere (vm-switch-boundary.md, section 1). +export function isJumpStep(node) { + return node?.instruction.operation.kind === 'jump' +} + +export class VmSwitchDecline extends Error { + constructor(code, message) { + super(message) + this.name = 'VmSwitchDecline' + this.code = code + } +} diff --git a/src/vm/switch/vm-switch-statements.js b/src/vm/switch/vm-switch-statements.js new file mode 100644 index 00000000..52bb68ed --- /dev/null +++ b/src/vm/switch/vm-switch-statements.js @@ -0,0 +1,1321 @@ +import { VmSwitchDecline, isJumpStep } from './vm-switch-model.js' + +// Statement recovery for one structured function of a VM switch model: the regions the +// structured emitter renders (loops, conditionals, a linear run, or the nodes no region covers). +// A node is one case; it reads only the typed operations and the control solver's result (its +// edges, reachability and handler stacks), so it needs no knowledge of the encoder that produced +// them (vm-switch-boundary.md, section 1). + +function fail(message) { + throw new VmSwitchDecline('invalid-structured-control', message) +} + +function blockGraph(blocks, edges) { + const byPc = new Map(blocks.map((block) => [block.startPc, block])) + const adjacency = new Map(blocks.map((block) => [block.id, []])) + for (const edge of edges) { + const sourceBlock = byPc.get(edge.sourcePc) + if (!sourceBlock) fail(`No block owns edge source ${edge.sourcePc}`) + if (edge.targetPc !== null) { + const targetBlock = byPc.get(edge.targetPc) + if (!targetBlock) fail(`No block owns edge target ${edge.targetPc}`) + adjacency.get(sourceBlock.id).push({ target: targetBlock.id, edge }) + } + } + return { byPc, adjacency } +} + +function reachableBlockIds(entry, adjacency) { + const visited = new Set([entry]) + const queue = [entry] + while (queue.length) { + const id = queue.shift() + for (const { target } of adjacency.get(id) ?? []) { + if (!visited.has(target)) { + visited.add(target) + queue.push(target) + } + } + } + return visited +} + +function dominators(blockIds, entry, adjacency) { + const all = new Set(blockIds) + const predecessors = new Map([...all].map((id) => [id, []])) + for (const [source, successors] of adjacency) { + for (const { target } of successors) + if (predecessors.has(target)) predecessors.get(target).push(source) + } + const dom = new Map( + [...all].map((id) => [id, id === entry ? new Set([entry]) : new Set(all)]), + ) + let changed = true + while (changed) { + changed = false + for (const id of all) { + if (id === entry) continue + const preds = predecessors.get(id).filter((pred) => all.has(pred)) + const intersection = preds.length + ? new Set( + preds.reduce( + (left, pred) => + [...left].filter((candidate) => dom.get(pred).has(candidate)), + [...all], + ), + ) + : new Set() + intersection.add(id) + if (!sameSet(dom.get(id), intersection)) { + dom.set(id, intersection) + changed = true + } + } + } + return { dom, predecessors } +} + +function sameSet(left, right) { + return ( + left.size === right.size && [...left].every((value) => right.has(value)) + ) +} + +// The statements a function nests: a loop entered at its header, a try at its setup. Statements +// are found walking forward: a case with more than one next step opens a control statement one +// level deeper, and a statement closes where the walk returns to its depth, so its end never +// depends on how the function ends. A statement's members are the blocks its entry dominates and +// its continuation does not. A loop closes where its own test exits: the first two-way branch on +// the header's straight run (while, for, for-in), or else the branch the back edge falls from +// (do-while), followed through jump-only blocks; a return or throw inside it is a transfer. A try +// closes where its own normal exit tail leads (a catch: the pop target its catch's completion +// reaches first, else the jump after a pop of this try; a finally: the literal a pop-first tail +// loads before jumping to its finalizer); a break or continue crossing it has the same tail, so a +// tail into an enclosing loop's head or end is a transfer when the try has several. Either +// continuation must leave a closed region: every edge into a member other than the entry comes +// from a member. +function findStatementRegions(entry, blocks, edges, graph, control) { + const blockById = new Map(blocks.map((block) => [block.id, block])) + const leafByPc = new Map( + blocks.map((block) => [block.startPc, block.instruction]), + ) + const { reachable } = control + const { dom, predecessors } = dominators(reachable, entry, graph.adjacency) + const dominates = (ancestor, id) => dom.get(id)?.has(ancestor) ?? false + const lastLeaf = (id) => blockById.get(id).instruction + const outgoing = (pc) => edges.filter((edge) => edge.sourcePc === pc) + const fallthroughPc = (pc) => { + const next = outgoing(pc) + return next.length === 1 && next[0].kind === 'fallthrough' + ? next[0].targetPc + : null + } + const membersOf = (head, continuation, cuts = []) => + new Set( + [...reachable].filter( + (id) => + dominates(head, id) && + ![continuation, ...cuts].some( + (cut) => cut && dominates(cut, id) && !dominates(cut, head), + ), + ), + ) + const closed = (head, members, kind) => + [...members].every((id) => + predecessors + .get(id) + .every( + (pred) => + !reachable.has(pred) || + members.has(pred) || + (id === head && (kind === 'try' || !dominates(head, pred))), + ), + ) + const normalNext = (id) => + graph.adjacency + .get(id) + .filter(({ edge }) => edge.route !== 'exceptional') + .map(({ target }) => target) + const jumpOnly = (id) => isJumpStep(blockById.get(id)) + const throughJumps = jumpStepResolver( + blocks, + normalAdjacency(graph.adjacency), + ) + const normalPredecessors = (id) => + (predecessors.get(id) ?? []).filter( + (pred) => reachable.has(pred) && normalNext(pred).includes(id), + ) + // A conditional that closes at `merge` and is one expression: its test's two arms reach only + // blocks it dominates, share none of them (so they first meet at the merge: an `if` whose arms + // meet earlier is a statement before the merge), leave only to the merge, every entry into the + // merge comes from them, and none of its blocks runs a statement (a try's head or tail, a + // return, a throw). + const statementKinds = new Set([ + 'catch-setup', + 'finally-setup', + 'handler-pop', + 'indirect-jump', + 'return', + 'throw', + 'for-in-next', + ]) + const expressionConditional = (merge, head) => { + const test = [...dom.get(merge)] + .filter((id) => id !== merge) + .sort((left, right) => dom.get(right).size - dom.get(left).size)[0] + if (!test || test === head || normalNext(test).length !== 2) return null + const blocks = new Set([test]) + const armOf = new Map() + for (const start of normalNext(test)) { + const work = start === merge ? [] : [start] + while (work.length) { + const id = work.pop() + if (armOf.get(id) === start) continue + if (armOf.has(id) || id === head || !dominates(test, id)) return null + armOf.set(id, start) + blocks.add(id) + for (const next of normalNext(id)) if (next !== merge) work.push(next) + } + } + if ( + normalPredecessors(merge).some((pred) => !blocks.has(pred)) || + [...blocks].some( + (id) => + (id !== test && + normalPredecessors(id).some((pred) => !blocks.has(pred))) || + statementKinds.has(blockById.get(id).instruction.operation.kind), + ) + ) + return null + return [...blocks] + } + // The run that ends at a loop's only back edge, when several paths enter it: where a continue + // that skips the rest of the body arrives (a for's update, or a do-while's test, which the walk + // starts from), unless a merge explains every entry. It walks back over straight blocks and + // over conditionals that are one expression (an update `i = x ? i + 2 : i + 1`), so it begins + // where the update or the test does. + const trailingRun = (head) => { + const latches = [...reachable].filter( + (id) => dominates(head, id) && normalNext(id).includes(head), + ) + if (latches.length !== 1) return [] + const run = bottomTests.has(head) + ? [bottomTests.get(head), latches[0]] + : [latches[0]] + for (;;) { + const preds = normalPredecessors(run[0]) + if (run[0] === head) return [] + if (preds.length > 1) { + const conditional = expressionConditional(run[0], head) + if (!conditional) return run + run.unshift(...conditional) + continue + } + if (preds.length !== 1 || normalNext(preds[0]).length !== 1) return [] + run.unshift(preds[0]) + } + } + const normalEdges = normalAdjacency(graph.adjacency) + const nestedClose = (id, head) => { + const outgoing = graph.adjacency.get(id) + const branch = outgoing.find(({ edge }) => edge.kind === 'conditional') + const fallthrough = outgoing.find(({ edge }) => edge.kind === 'fallthrough') + if (!branch || !fallthrough) return null + const stops = new Set([ + head, + ...regions + .filter((region) => region.kind === 'loop' && region.members.has(head)) + .flatMap((region) => [region.head, region.continuation]) + .filter(Boolean), + ]) + const { merge } = closeConditional( + id, + branch.target, + fallthrough.target, + normalEdges, + stops, + throughJumps, + ) + return merge && merge !== head ? merge : null + } + // Where a loop closes: its test's target outside the cycle, through jump-only blocks. + // A loop whose test is the branch its back edge falls from (a do-while), by its header. + const bottomTests = new Map() + const loopEnd = (head) => { + const latches = [...reachable].filter( + (id) => dominates(head, id) && normalNext(id).includes(head), + ) + const cycle = new Set([head]) + const work = [...latches] + while (work.length) { + const id = work.pop() + if (cycle.has(id)) continue + cycle.add(id) + for (const pred of predecessors.get(id) ?? []) + if (reachable.has(pred)) work.push(pred) + } + const exitOf = (id) => { + const next = normalNext(id) + const kind = blockById.get(id).instruction.operation.kind + if (!['branch', 'for-in-next'].includes(kind) || next.length !== 2) + return null + const outside = next.filter((target) => !cycle.has(target)) + if (outside.length !== 1) return null + let target = outside[0] + const seen = new Set() + while (jumpOnly(target) && !seen.has(target)) { + seen.add(target) + const following = normalNext(target) + if (following.length !== 1 || cycle.has(following[0])) break + target = following[0] + } + return target + } + let at = head + const walked = new Set() + while (!walked.has(at)) { + walked.add(at) + const next = normalNext(at) + if (next.length !== 1) { + const end = exitOf(at) + if (end) return end + // A branch whose arms close inside the cycle is nested in the test (a short-circuit + // operand, say): the walk continues where it closes. + const close = nestedClose(at, head) + if (!close) break + at = close + continue + } + at = next[0] + } + if (latches.length !== 1) return null + const before = (predecessors.get(latches[0]) ?? []).filter((id) => + cycle.has(id), + ) + const end = before.length === 1 ? exitOf(before[0]) : null + if (end) bottomTests.set(head, before[0]) + return end + } + // The pops of a try's own handler record: `[pc, record]` for each. + const popsOf = (head) => { + const setupPc = lastLeaf(head).pc + return [...leafByPc] + .filter( + ([pc, leaf]) => + leaf.operation.kind === 'handler-pop' && + control.stateIn.get(pc)?.at(-1)?.setupPc === setupPc, + ) + .map(([pc]) => [pc, control.stateIn.get(pc).at(-1)]) + } + // The block a route leads to through jump steps only it enters: a jump step that several + // routes enter is where they meet, a location (a try nested in a catch ends at the jump that + // its own pops and its catch share). + const ownSteps = (id) => { + const seen = new Set() + while ( + jumpOnly(id) && + !seen.has(id) && + normalNext(id).length === 1 && + normalPredecessors(id).length === 1 + ) { + seen.add(id) + id = normalNext(id)[0] + } + return id + } + // Where a catch-only try completes when its catch completes normally: a native try statement + // ends where its block's normal end and its catch's normal end meet. Each pop is followed + // through its own jump steps to the block it leads to; of those the catch's own normal walk + // (never passing the setup again, so a loop's next iteration is not the catch completing) + // reaches, the end is the one it reaches first, which dominates the others over that walk. A + // pop of a break or continue crossing the try leads to a block the catch reaches only through + // the end, or to an enclosing loop's head or end, which is never the end. Null when the catch + // never completes normally. + const catchEnd = (head, enclosing) => { + const setup = lastLeaf(head).operation + const handler = graph.byPc.get(setup.target)?.id + if (setup.kind !== 'catch-setup' || !handler) return null + const walk = new Set() + const queue = [handler] + for (const id of queue) { + if (walk.has(id) || id === head) continue + walk.add(id) + queue.push(...normalNext(id)) + } + const reached = [ + ...new Set( + popsOf(head) + .map(([pc]) => fallthroughPc(pc)) + .filter((pc) => pc != null) + .map((pc) => ownSteps(graph.byPc.get(pc).id)), + ), + ].filter((id) => walk.has(id) && !enclosing.has(id)) + const { dom } = dominators(walk, handler, normalEdges) + return ( + reached.find((id) => reached.every((other) => dom.get(other).has(id))) ?? + null + ) + } + // Targets of a try's normal exit tails, found from the pops of its own handler record. + const exitTails = (head) => { + const targets = new Set() + for (const [pc, record] of popsOf(head)) { + const next = leafByPc.get(fallthroughPc(pc)) + if (record.kind === 'catch') { + // A catch that never completes normally: the target of the compiler's exit jump. A pop + // that falls into code (a return crossing the try) ends nothing; a reordered normal exit + // of such a catch that dropped the jump has the same graph. + const jump = + next?.operation.kind === 'jump' ? outgoing(next.pc)[0] : null + if (jump?.targetPc != null) targets.add(jump.targetPc) + continue + } + const jump = next && leafByPc.get(fallthroughPc(next.pc)) + if ( + next?.operation.kind === 'assign' && + next.operation.destination === record.continuation && + next.operation.value.kind === 'literal' && + jump?.operation.kind === 'jump' && + outgoing(jump.pc)[0]?.targetPc === record.target + ) + targets.add(next.operation.value.value) + } + return new Set( + [...targets].map((pc) => graph.byPc.get(pc)?.id).filter(Boolean), + ) + } + const entries = [] + for (const [source, successors] of graph.adjacency) + for (const { target } of successors) + if ( + reachable.has(source) && + dominates(target, source) && + !entries.some((item) => item.head === target) + ) + entries.push({ kind: 'loop', head: target }) + for (const id of reachable) { + const kind = lastLeaf(id).operation.kind + if (kind === 'catch-setup' || kind === 'finally-setup') + entries.push({ kind: 'try', head: id }) + } + // Outermost first; a loop whose header is a try's setup encloses that try. + entries.sort( + (left, right) => dom.get(left.head).size - dom.get(right.head).size, + ) + const regions = [] + for (const item of entries) { + let continuation = null + let layoutEnd = null + let cuts = [] + let loopsOutside = [] + if (item.kind === 'loop') { + continuation = loopEnd(item.head) + } else { + // A break or continue crossing the try has the same tail as its normal end. + const enclosing = new Set( + regions + .filter( + (region) => region.kind === 'loop' && region.members.has(item.head), + ) + .flatMap((region) => [region.head, region.continuation]), + ) + const ended = catchEnd(item.head, enclosing) + let tails = ended ? [ended] : [...exitTails(item.head)] + if (tails.length > 1) tails = tails.filter((id) => !enclosing.has(id)) + // A continue to an enclosing loop's trailing run is a transfer too (a catch's tail, or a + // finally's routed pad), unless the try's normal end arrives there as well. + loopsOutside = regions.filter( + (region) => + region.kind === 'loop' && + region.members.has(item.head) && + region.trailing.length && + !dominates(region.trailing[0], item.head), + ) + cuts = loopsOutside.map((region) => region.trailing[0]) + const continued = (id) => + loopsOutside.find((region) => region.trailing[0] === throughJumps(id)) + if (tails.length > 1) { + const rest = tails.filter((id) => !continued(id)) + if (rest.length) tails = rest + } + // The compiler emits one normal tail per try; another (a reachable injected copy, say) + // leaves through the first, and the rest are walked as routed exits. + if (tails.length) + continuation = tails.sort( + (left, right) => + blockById.get(left).startPc - blockById.get(right).startPc, + )[0] + } + if ( + continuation && + !closed(item.head, membersOf(item.head, continuation, cuts), item.kind) + ) + continuation = null + // A catch whose block never completes normally has no reachable end; the frontend's layout + // end places one where the catch's own normal walk arrives inside the catch. + const setup = lastLeaf(item.head).operation + if ( + item.kind === 'try' && + !continuation && + setup.kind === 'catch-setup' && + setup.layoutEnd !== undefined + ) { + const handler = graph.byPc.get(setup.target)?.id + const end = graph.byPc.get(setup.layoutEnd)?.id + if ( + handler && + end && + reachable.has(end) && + dominates(handler, end) && + reachableBlockIds(handler, normalEdges).has(end) + ) + layoutEnd = end + } + if (continuation) + for (const loop of loopsOutside) { + const target = loop.trailing[0] + if ( + target !== continuation && + [...membersOf(item.head, continuation, cuts)].some((id) => + normalNext(id).includes(target), + ) + ) + loop.continues.push(item.head) + } + regions.push({ + ...item, + continuation, + members: membersOf(item.head, continuation ?? layoutEnd, cuts), + ...(item.kind === 'try' ? { layoutEnd } : {}), + ...(item.kind === 'loop' + ? { + trailing: trailingRun(item.head), + trailingIsTest: bottomTests.has(item.head), + continues: [], + } + : {}), + }) + } + return regions +} + +function findNaturalLoops( + entry, + blocks, + edges, + graph, + statementRegions, + control, +) { + const startPc = new Map(blocks.map((block) => [block.id, block.startPc])) + const { reachable } = control + const { dom } = dominators(reachable, entry, graph.adjacency) + const statements = new Map( + statementRegions + .filter(({ kind }) => kind === 'loop') + .map((region) => [region.head, region]), + ) + const loops = [] + for (const [source, successors] of graph.adjacency) { + for (const { target } of successors) { + if ( + !reachable.has(source) || + !reachable.has(target) || + !dom.get(source)?.has(target) || + source === target + ) + continue + const region = statements.get(target) + const members = region.members + const exitEdges = edges.filter((candidate) => { + const candidateSource = graph.byPc.get(candidate.sourcePc)?.id + const candidateTarget = + candidate.targetPc === null + ? null + : graph.byPc.get(candidate.targetPc)?.id + return ( + members.has(candidateSource) && + (candidateTarget === null || !members.has(candidateTarget)) + ) + }) + const headerEdges = edges.filter( + (candidate) => + graph.byPc.get(candidate.sourcePc)?.id === target && + candidate.targetPc !== null && + members.has(graph.byPc.get(candidate.targetPc)?.id), + ) + if (!exitEdges.length || !headerEdges.length) continue + const key = `${target}:${[...members].sort().join(',')}` + if (loops.some((loop) => loop.key === key)) continue + loops.push({ + key, + kind: 'natural-loop', + headerBlockId: target, + bodyBlockIds: [...members].sort( + (left, right) => startPc.get(left) - startPc.get(right), + ), + continuationBlockId: region.continuation, + trailing: region.trailing, + trailingIsTest: region.trailingIsTest, + continues: region.continues, + headerCondition: + headerEdges.find((candidate) => candidate.kind === 'conditional') + ?.condition ?? null, + proof: + 'header dominates the back-edge source, the body is the header statement region, and the loop has an explicit exit', + }) + } + } + return loops.map((loop) => { + const result = { ...loop } + delete result.key + return result + }) +} + +// Where flow entering `id` arrives once it has passed its jump steps. +function jumpStepResolver(blocks, normal) { + const blockById = new Map(blocks.map((block) => [block.id, block])) + return (id) => { + const seen = new Set() + while ( + isJumpStep(blockById.get(id)) && + !seen.has(id) && + normal.get(id)?.length === 1 + ) { + seen.add(id) + id = normal.get(id)[0].target + } + return id + } +} + +// A conditional's arms reconverge along normal control flow. An exceptional route into a +// handler or finalizer leaves the statement instead of joining its arms, as +// terminalBranchBlocks treats it, so the reconvergence search does not follow one. +function normalAdjacency(adjacency) { + return new Map( + [...adjacency].map(([id, successors]) => [ + id, + successors.filter(({ edge }) => edge.route !== 'exceptional'), + ]), + ) +} + +// Where a control statement opened at a two-way branch closes: the first block both arms reach, +// each arm walking forward over normal edges and stopping at a transfer (an edge into one of +// `stops`, an enclosing loop's head or end). "First" is dominance over paths from the test through +// its arms only, so code the arms share with paths from outside the statement does not hide where +// they meet. If the arms never meet, an arm that starts at a stop closes the statement there; +// otherwise the fallthrough arm is the consequent and the branch target, the else-or-end, is the +// end. Arms that meet without a first block close nowhere. +function closeConditional( + testId, + branchId, + fallthroughId, + normal, + stops, + throughJumps, +) { + const reached = new Map() + const arm = (start) => { + const seen = new Set() + const ends = new Set(stops.has(start) ? [start] : []) + const work = stops.has(start) ? [] : [start] + while (work.length) { + const id = work.pop() + if (seen.has(id)) continue + seen.add(id) + for (const { target } of normal.get(id) ?? []) + if (stops.has(target)) ends.add(target) + else work.push(target) + } + reached.set(start, ends) + return seen + } + const branchArm = arm(branchId) + const fallthroughArm = arm(fallthroughId) + const common = [...branchArm].filter((id) => fallthroughArm.has(id)) + const sharedStops = [...reached.get(branchId)].filter((id) => + reached.get(fallthroughId).has(id), + ) + if (!common.length && sharedStops.length === 1) + return { + merge: sharedStops[0], + proof: 'the arms meet only at a stop, so the statement closes there', + } + // The branch target is the else-or-end, at the stop its jump steps lead straight to. + const end = stops.has(throughJumps(branchId)) + ? throughJumps(branchId) + : branchId + if (!common.length) + return { + merge: stops.has(fallthroughId) ? fallthroughId : end, + proof: + 'the arms never meet: the transferring arm ends inside the statement', + } + const inside = new Set([testId, ...branchArm, ...fallthroughArm]) + const { dom } = dominators( + inside, + testId, + new Map( + [...inside].map((id) => [ + id, + (normal.get(id) ?? []).filter(({ target }) => inside.has(target)), + ]), + ), + ) + return { + merge: + common.find((candidate) => + common.every((id) => dom.get(id)?.has(candidate)), + ) ?? null, + proof: 'the merge is the first block both arms reach before a transfer', + } +} + +// The registers an operation writes. +function writtenRegisters(operation) { + if (operation.kind === 'catch-setup') return [operation.exceptionRegister] + if (operation.kind === 'finally-setup') + return [operation.continuation, operation.payload] + return Number.isSafeInteger(operation.destination) + ? [operation.destination] + : [] +} + +const RUN_BREAKING_KINDS = new Set([ + 'branch', + 'indirect-jump', + 'for-in-next', + 'return', + 'throw', + 'catch-setup', + 'finally-setup', + 'handler-pop', +]) + +// A switch is a chain of two-way tests that compare one register, the discriminant, with `===` +// against each case's value, in order, while nothing in the chain writes it: a test's match route +// enters its case body, its miss route runs the next case's value into the next test, and the +// last miss enters the default body or the end. That is how a native switch selects a clause, so +// the chain is one when its case bodies share routes a conditional cannot express (a body falling +// into the next one, a default in the middle): a test in it whose arms meet without a first +// block. The switch ends at the block every body's normal exit reaches; its clauses are ordered so +// that each body falling into another is followed by it, a default placed where that holds. +function findSwitchRegions(blocks, edges, graph, loops, control) { + const normal = normalAdjacency(graph.adjacency) + const throughJumps = jumpStepResolver(blocks, normal) + const blockById = new Map(blocks.map((block) => [block.id, block])) + const startPc = new Map(blocks.map((block) => [block.id, block.startPc])) + const { reachable } = control + const predecessors = new Map(blocks.map((block) => [block.id, []])) + for (const [source, successors] of normal) + if (reachable.has(source)) + for (const { target } of successors) predecessors.get(target).push(source) + const captured = new Set( + blocks.flatMap(({ instruction: { operation } }) => + operation.kind === 'closure' + ? operation.captures + .filter(({ kind }) => kind === 'local') + .map(({ index }) => index) + : [], + ), + ) + const stopsOf = (id) => + new Set( + loops + .filter((loop) => loop.bodyBlockIds.includes(id)) + .flatMap((loop) => [ + loop.headerBlockId, + loop.continuationBlockId, + loop.trailing[0], + ]) + .filter(Boolean), + ) + const next = (id) => + normal.get(id)?.length === 1 ? normal.get(id)[0].target : null + const routes = (id) => { + const kind = blockById.get(id).instruction.operation.kind + return kind === 'catch-setup' || kind === 'finally-setup' + ? graph.adjacency.get(id) + : (normal.get(id) ?? []) + } + const stepsTo = (id) => { + const steps = [] + while ( + isJumpStep(blockById.get(id)) && + !steps.includes(id) && + next(id) !== null + ) { + steps.push(id) + id = next(id) + } + return { entry: id, steps } + } + const twoWay = (id) => { + const leaf = blockById.get(id).instruction + if (!reachable.has(id) || leaf.operation.kind !== 'branch') return null + const outgoing = edges.filter((edge) => edge.sourcePc === leaf.pc) + const conditional = outgoing.find(({ kind }) => kind === 'conditional') + const fallthrough = outgoing.find(({ kind }) => kind === 'fallthrough') + if ( + !conditional || + !fallthrough || + conditional.targetPc === null || + fallthrough.targetPc === null + ) + return null + const [match, miss] = + leaf.operation.when === 'false' + ? [fallthrough, conditional] + : [conditional, fallthrough] + return { + testBlockId: id, + condition: leaf.operation.condition, + match: graph.byPc.get(match.targetPc).id, + miss: graph.byPc.get(miss.targetPc).id, + branchId: graph.byPc.get(conditional.targetPc).id, + fallthroughId: graph.byPc.get(fallthrough.targetPc).id, + } + } + // The operands of the `===` whose result the branch reads, when nothing after it writes the + // result or either operand. + const compared = (operations, condition) => { + const index = operations.findLastIndex((operation) => + writtenRegisters(operation).includes(condition), + ) + const operation = operations[index] + if ( + operation?.kind !== 'assign' || + operation.value.kind !== 'binary' || + operation.value.operator !== '===' + ) + return null + const operands = [operation.value.left, operation.value.right] + if ( + operations + .slice(index + 1) + .some((later) => + writtenRegisters(later).some( + (register) => register === condition || operands.includes(register), + ), + ) + ) + return null + return operands + } + // The head's comparison may come from the straight run that enters its test. + const headOperands = (test) => { + let operations = [] + let current = test.testBlockId + for (;;) { + const operands = compared(operations, test.condition) + if (operands) return operands + const preds = predecessors.get(current) + if (preds.length !== 1 || next(preds[0]) !== current) return null + current = preds[0] + operations = [blockById.get(current).instruction.operation, ...operations] + } + } + // The next case: the straight run from a miss route to the next test, entered only from it. + const nextCase = (test) => { + const runBlockIds = [] + let current = test.miss + let from = test.testBlockId + while (!runBlockIds.includes(current)) { + const preds = predecessors.get(current) + if (preds.length !== 1 || preds[0] !== from) return null + const following = twoWay(current) + if (following) { + const operations = [ + ...runBlockIds.map((id) => blockById.get(id).instruction.operation), + ] + if (operations.some(({ kind }) => RUN_BREAKING_KINDS.has(kind))) + return null + const operands = compared(operations, following.condition) + return operands + ? { ...following, runBlockIds, operands, operations } + : null + } + if (next(current) === null) return null + runBlockIds.push(current) + from = current + current = next(current) + } + return null + } + // The longest chain from `head` whose tests share one compared register and never write it. + const chainFrom = (head) => { + const operands = headOperands(head) + if (!operands) return null + const chain = [{ ...head, runBlockIds: [], operands, operations: [] }] + let candidates = new Set(operands) + for (;;) { + const following = nextCase(chain.at(-1)) + if (!following) break + const shared = following.operands.filter((register) => + candidates.has(register), + ) + if (!shared.length) break + candidates = new Set(shared) + chain.push(following) + } + // The discriminant is the compared register the chain never writes; a case value register is + // written by the runs, often one register reused for every value. + while (chain.length >= 2) { + const written = new Set( + chain.flatMap(({ operations }) => + operations.flatMap((operation) => writtenRegisters(operation)), + ), + ) + const unwritten = [...candidates].filter( + (register) => !written.has(register), + ) + const discriminant = unwritten.length === 1 ? unwritten[0] : null + if ( + discriminant !== null && + (!captured.has(discriminant) || + chain.every(({ operations }) => + operations.every(({ kind }) => kind === 'assign'), + )) + ) + return { chain, discriminant } + chain.pop() + candidates = new Set( + chain + .map(({ operands }) => operands) + .reduce((left, right) => + left.filter((value) => right.includes(value)), + ), + ) + } + return null + } + + const tests = blocks.map(({ id }) => twoWay(id)).filter(Boolean) + const chains = tests + .map((test) => ({ head: test, found: chainFrom(test) })) + .filter(({ found }) => found) + const continued = new Set( + chains.flatMap(({ found }) => + found.chain.slice(1).map(({ testBlockId }) => testBlockId), + ), + ) + const regions = [] + for (const { head, found } of chains) { + if (continued.has(head.testBlockId)) continue + const { chain, discriminant } = found + const stops = stopsOf(head.testBlockId) + const unmerged = chain.some( + (test) => + !closeConditional( + test.testBlockId, + test.branchId, + test.fallthroughId, + normal, + stopsOf(test.testBlockId), + throughJumps, + ).merge, + ) + if (!unmerged) continue + const cases = chain.map((test) => { + const { entry: entryBlockId, steps } = stepsTo(test.match) + return { + testBlockId: test.testBlockId, + value: test.operands.find((register) => register !== discriminant), + runBlockIds: test.runBlockIds, + stepBlockIds: steps, + entryBlockId, + } + }) + if (cases.some(({ value }) => value === undefined)) continue + const miss = stepsTo(chain.at(-1).miss) + const chainBlocks = new Set([ + ...cases.flatMap(({ testBlockId, runBlockIds, stepBlockIds }) => [ + testBlockId, + ...runBlockIds, + ...stepBlockIds, + ]), + ...miss.steps, + ]) + const entries = [ + ...new Set([ + ...cases.map(({ entryBlockId }) => entryBlockId), + miss.entry, + ]), + ] + // Members for an end: what the bodies reach before it or a stop, closed against entry from + // anywhere but the chain. A try inside a body is a member with its catch or finalizer, which + // its setup's own handler route enters. + const membersFor = (end) => { + const members = new Set() + const work = entries.filter((id) => id !== end && !stops.has(id)) + while (work.length) { + const id = work.pop() + if (members.has(id)) continue + members.add(id) + for (const { target } of routes(id)) + if (target !== end && !stops.has(target)) work.push(target) + } + const closed = [...members].every( + (id) => + !chainBlocks.has(id) && + predecessors + .get(id) + .every((pred) => members.has(pred) || chainBlocks.has(pred)), + ) + return closed ? members : null + } + const everything = membersFor(null) + const candidates = [ + ...new Set([ + ...[...(everything ?? [])].flatMap((id) => + (normal.get(id) ?? []).map(({ target }) => target), + ), + ...entries, + ]), + ] + // The end is the nearest such block: the one leaving the fewest members. With none, every + // body leaves by a transfer. + let end = null + let members = everything + for (const candidate of candidates) { + const found = membersFor(candidate) + if (!found || found.has(candidate)) continue + if (end === null || found.size < members.size) { + end = candidate + members = found + } + } + if (members === null) continue + // Which entry each body falls into: the one other entry its walk reaches before the end. + const fallsInto = new Map() + let shared = false + for (const start of entries) { + if (start === end) continue + const reached = new Set() + const seen = new Set() + const work = [start] + while (work.length) { + const id = work.pop() + if (seen.has(id)) continue + seen.add(id) + for (const { target } of normal.get(id) ?? []) { + if (target === end || stops.has(target)) continue + if (entries.includes(target) && target !== start) reached.add(target) + else work.push(target) + } + } + if (reached.size > 1) shared = true + if (reached.size === 1) fallsInto.set(start, [...reached][0]) + } + if (shared) continue + const caseClauses = cases.map(({ entryBlockId }, caseIndex) => ({ + caseIndex, + entryBlockId, + })) + const hasDefault = miss.entry !== end + const fits = (clauses) => { + const groups = [] + for (const clause of clauses) + if (groups.at(-1)?.entryBlockId === clause.entryBlockId) + groups.at(-1).clauses.push(clause) + else + groups.push({ entryBlockId: clause.entryBlockId, clauses: [clause] }) + if ( + new Set(groups.map(({ entryBlockId }) => entryBlockId)).size !== + groups.length + ) + return false + return groups.every( + ({ entryBlockId }, index) => + !fallsInto.has(entryBlockId) || + groups[index + 1]?.entryBlockId === fallsInto.get(entryBlockId), + ) + } + let clauses = null + for (let position = caseClauses.length; position >= 0; position -= 1) { + const candidate = hasDefault + ? [ + ...caseClauses.slice(0, position), + { caseIndex: null, entryBlockId: miss.entry }, + ...caseClauses.slice(position), + ] + : caseClauses + if (fits(candidate)) { + clauses = candidate + break + } + if (!hasDefault) break + } + if (!clauses) continue + regions.push({ + kind: 'switch', + testBlockId: head.testBlockId, + discriminant, + cases, + defaultEntryBlockId: hasDefault ? miss.entry : null, + defaultStepBlockIds: miss.steps, + clauses, + continuationBlockId: end, + memberBlockIds: [...members].sort( + (left, right) => startPc.get(left) - startPc.get(right), + ), + proof: + 'a chain of === tests on one unchanged register whose case bodies share routes; each body falling into another is followed by it', + }) + } + return regions +} + +// A conditional closes where the walk returns to its depth: the first block both arms reach, +// each arm's walk stopping at a transfer (a return or throw, or an edge to an enclosing loop's +// head or end). If the arms never meet, an arm that starts at such a head or end (a loop's own +// test leaving it) closes there; otherwise the fallthrough arm is the consequent and the branch +// target, the else-or-end, is the end. An exceptional route into a handler or finalizer leaves +// the statement rather than joining its arms, so it is never followed. +function findConditionalRegions(blocks, edges, graph, loops, switches) { + const normal = normalAdjacency(graph.adjacency) + const throughJumps = jumpStepResolver(blocks, normal) + const switchTests = new Set( + switches.flatMap(({ cases }) => + cases.map(({ testBlockId }) => testBlockId), + ), + ) + const regions = [] + for (const block of blocks) { + if (switchTests.has(block.id)) continue + const outgoing = edges.filter((edge) => edge.sourcePc === block.startPc) + const conditional = outgoing.find((edge) => edge.kind === 'conditional') + const fallthrough = outgoing.find((edge) => edge.kind === 'fallthrough') + if ( + !conditional || + !fallthrough || + conditional.targetPc === null || + fallthrough.targetPc === null + ) + continue + const branchBlock = graph.byPc.get(conditional.targetPc) + const fallthroughBlock = graph.byPc.get(fallthrough.targetPc) + if (!branchBlock || !fallthroughBlock) continue + const stops = new Set( + loops + .filter((loop) => loop.bodyBlockIds.includes(block.id)) + .flatMap((loop) => [ + loop.headerBlockId, + loop.continuationBlockId, + loop.trailing[0], + ]) + .filter(Boolean), + ) + // Inside a switch body, its end is a break and another clause's entry a fallthrough. + for (const region of switches) + if (region.memberBlockIds.includes(block.id)) + for (const id of [ + region.continuationBlockId, + ...region.clauses.map(({ entryBlockId }) => entryBlockId), + ]) + if (id) stops.add(id) + const { merge, proof } = closeConditional( + block.id, + branchBlock.id, + fallthroughBlock.id, + normal, + stops, + throughJumps, + ) + if (!merge) continue + regions.push({ + kind: 'conditional', + testBlockId: block.id, + condition: conditional.condition, + branchTargetPc: conditional.targetPc, + fallthroughTargetPc: fallthrough.targetPc, + mergeBlockId: merge, + proof, + }) + } + return regions +} + +// A loop's trailing run is where its continues arrive when a statement inside the loop jumps +// there without closing there: a try that closes elsewhere while one of its blocks jumps to the +// run, or a conditional whose arm reaches the run while the conditional closes elsewhere and no +// inner loop holding it ends at the run. Only then is the run the loop's update, published as its +// continue target; otherwise the run is ordinary body code. +function publishContinueTargets(loops, conditionals, graph) { + const normal = normalAdjacency(graph.adjacency) + const reaches = (start, blocked, goal) => { + const seen = new Set() + const work = [start] + while (work.length) { + const id = work.pop() + if (id === goal) return true + if (seen.has(id) || blocked.has(id)) continue + seen.add(id) + for (const { target } of normal.get(id) ?? []) work.push(target) + } + return false + } + for (const loop of loops) { + const target = loop.trailing[0] ?? null + const stopsOf = (testBlockId) => + new Set( + loops + .filter((other) => other.bodyBlockIds.includes(testBlockId)) + .flatMap((other) => [ + other.headerBlockId, + other.continuationBlockId, + other.trailing[0], + ]) + .filter(Boolean), + ) + const routed = + target !== null && + (loop.continues.length > 0 || + conditionals.some((conditional) => { + if (!loop.bodyBlockIds.includes(conditional.testBlockId)) return false + // An inner loop that ends at the run explains a jump to it from inside: its break. + if ( + loops.some( + (inner) => + inner !== loop && + inner.continuationBlockId === target && + inner.bodyBlockIds.includes(conditional.testBlockId), + ) + ) + return false + const stops = stopsOf(conditional.testBlockId) + if (stops.has(conditional.mergeBlockId)) return false + const blocked = new Set([...stops, conditional.mergeBlockId]) + blocked.delete(target) + return conditional.startIds.some((start) => + reaches(start, blocked, target), + ) + })) + loop.continueBlockId = routed ? target : null + loop.updateBlockIds = routed && !loop.trailingIsTest ? loop.trailing : null + loop.testBlockIds = routed && loop.trailingIsTest ? loop.trailing : null + } + for (const loop of loops) { + delete loop.trailing + delete loop.trailingIsTest + delete loop.continues + } +} + +// The regions of one structured function, in the order the emitter indexes them: a linear run +// when every edge falls through or returns; otherwise conditionals, then loops, then one opaque +// row listing every block that is neither a conditional test nor a loop header. Reachability and +// handler state come from the control solver's result (`solveControl`). +export function planStatements(functionRecord, cases, solved) { + const blocks = [...cases] + .sort((left, right) => left.pc - right.pc) + .map((instruction) => ({ + id: `${functionRecord.id}:b@${instruction.pc}`, + startPc: instruction.pc, + instruction, + })) + const { edges } = solved + const blockOrder = blocks.map(({ id }) => id) + if ( + edges.every( + (edge) => edge.kind === 'fallthrough' || edge.kind === 'return', + ) && + !blocks.some((block) => + ['catch-setup', 'finally-setup'].includes( + block.instruction.operation.kind, + ), + ) + ) + return [ + { + kind: 'linear', + blockIds: blockOrder, + proof: + 'every reachable successor is the next serialized block or an explicit return', + }, + ] + const entry = `${functionRecord.id}:b@${functionRecord.startPc}` + const graph = blockGraph(blocks, edges) + const reachablePcs = new Set(solved.reachablePcs) + const control = { + reachable: new Set( + blocks + .filter((block) => reachablePcs.has(block.startPc)) + .map(({ id }) => id), + ), + stateIn: solved.stateIn, + } + const statements = findStatementRegions(entry, blocks, edges, graph, control) + const loops = findNaturalLoops( + entry, + blocks, + edges, + graph, + statements, + control, + ) + const startPc = new Map(blocks.map((block) => [block.id, block.startPc])) + const tries = statements + .filter(({ kind }) => kind === 'try') + .map((statement) => ({ + kind: 'try', + setupBlockId: statement.head, + continuationBlockId: statement.continuation, + layoutEndBlockId: statement.layoutEnd, + memberBlockIds: [...statement.members].sort( + (left, right) => startPc.get(left) - startPc.get(right), + ), + proof: + 'the setup dominates every member; the continuation is where its normal exit tail leads', + })) + const switches = findSwitchRegions(blocks, edges, graph, loops, control) + const conditionals = findConditionalRegions( + blocks, + edges, + graph, + loops, + switches, + ) + // A switch takes part as a statement whose arms are its clause bodies and the default route. + publishContinueTargets( + loops, + [ + ...conditionals.map((conditional) => ({ + testBlockId: conditional.testBlockId, + mergeBlockId: conditional.mergeBlockId, + startIds: [ + conditional.branchTargetPc, + conditional.fallthroughTargetPc, + ].map((pc) => graph.byPc.get(pc).id), + })), + ...switches.map((region) => ({ + testBlockId: region.testBlockId, + mergeBlockId: region.continuationBlockId, + startIds: region.clauses.map(({ entryBlockId }) => entryBlockId), + })), + ], + graph, + ) + const regions = [...conditionals, ...switches, ...loops] + const covered = new Set( + regions.flatMap((region) => + region.kind === 'conditional' + ? [region.testBlockId] + : region.kind === 'switch' + ? region.cases.map(({ testBlockId }) => testBlockId) + : [region.headerBlockId], + ), + ) + const unsupported = blockOrder.filter((id) => !covered.has(id)) + if (unsupported.length) + regions.push({ + kind: 'opaque', + blockIds: unsupported, + reason: 'control shape not structurally proved by the accepted CFG', + }) + regions.push(...tries) + return regions +} diff --git a/src/vm/switch/vm-switch-structured-emitter.js b/src/vm/switch/vm-switch-structured-emitter.js new file mode 100644 index 00000000..faadf68c --- /dev/null +++ b/src/vm/switch/vm-switch-structured-emitter.js @@ -0,0 +1,1321 @@ +import { parse } from '@babel/parser' +import { + appendRendered, + decline, + effectiveStructuredInstructions, + emitInstruction, + functionName, + functionNeedsArguments, + functionRegisterSet, + invertPredicate, + localCaptureRegisterSet, + registerName, + requireValue, + retargetDestination, +} from './vm-switch-copy-planning.js' +import { isJumpStep } from './vm-switch-model.js' + +const RUN_ENDING_KINDS = new Set([ + 'jump', + 'branch', + 'for-in-next', + 'indirect-jump', + 'return', + 'throw', +]) +const CONTROL_KINDS = new Set([ + ...RUN_ENDING_KINDS, + 'catch-setup', + 'handler-pop', + 'finally-setup', +]) + +class StructuredFunctionEmitter { + constructor( + functionId, + instructions, + functionRecord, + regions, + functions, + copyPlan, + coalescedMovePcs, + solved, + ) { + this.functionId = functionId + this.instructions = instructions + this.functionRecord = functionRecord + // A node is one case, named as the planner names it; its successors are the control + // solver's edges from its PC. + this.nodeById = new Map( + instructions.map((instruction) => { + const id = `${functionId}:b@${instruction.pc}` + return [id, { id, startPc: instruction.pc, instruction }] + }), + ) + this.functions = functions + this.copyPlan = copyPlan + this.coalescedMovePcs = coalescedMovePcs + this.reachablePcs = new Set(solved.reachablePcs) + this.stateIn = solved.stateIn + this.solvedEdgesBySource = Map.groupBy( + solved.edges, + (edge) => edge.sourcePc, + ) + this.conditionalByTest = new Map( + regions + .filter(({ kind }) => kind === 'conditional') + .map((region) => [region.testBlockId, region]), + ) + this.tryBySetup = new Map( + regions + .filter(({ kind }) => kind === 'try') + .map((region) => [region.setupBlockId, region]), + ) + this.loopByHeader = new Map( + regions + .filter(({ kind }) => kind === 'natural-loop') + .map((region) => [region.headerBlockId, region]), + ) + this.switchByHead = new Map( + regions + .filter(({ kind }) => kind === 'switch') + .map((region) => [region.testBlockId, region]), + ) + this.emittedBlocks = new Set() + this.loopNumber = 0 + this.switchNumber = 0 + } + + nodeForPc(pc) { + return requireValue( + this.nodeById.get(`${this.functionId}:b@${pc}`), + 'invalid-structured-control', + `Structured control has no block for target ${pc}`, + ) + } + + outgoing(node) { + return this.solvedEdgesBySource.get(node.startPc) ?? [] + } + + targetBlock(edge) { + return edge.targetPc === null ? null : this.nodeForPc(edge.targetPc).id + } + + // A jump step runs nothing; readers that follow control look through it. + isJumpStep(node) { + return isJumpStep(node) + } + + markBlock(block) { + if (this.emittedBlocks.has(block.id)) + decline( + 'unemittable-structured-control', + `Structured block ${block.id} would be emitted more than once`, + ) + this.emittedBlocks.add(block.id) + } + + // context.loops holds every breakable statement; a switch is one, but never a loop. + activeLoop(context) { + return context.loops.findLast(({ kind }) => kind !== 'switch') ?? null + } + + loopTransfer(context, target) { + for (let index = context.loops.length - 1; index >= 0; index -= 1) { + const loop = context.loops[index] + if (target === loop.headerBlockId || target === loop.continueBlockId) + return { kind: 'continue', loop, target } + if (loop.exitTargets.has(target)) return { kind: 'break', loop, target } + } + return null + } + + edgeTo(block, kind) { + return this.outgoing(block).find(({ kind: edgeKind }) => edgeKind === kind) + } + + renderInstruction(instruction, lines) { + if (instruction.coalesced || this.coalescedMovePcs.has(instruction.pc)) + return + if (this.copyPlan.has(instruction.pc)) + instruction = retargetDestination( + instruction, + this.copyPlan.get(instruction.pc), + ) + appendRendered( + lines, + emitInstruction( + this.functionId, + instruction, + this.functions, + 'structured', + ), + ) + } + + renderConditional(region, context, stopIds) { + const test = this.nodeById.get(region.testBlockId) + requireValue( + test, + 'invalid-structured-control', + `Conditional region refers to missing test block ${region.testBlockId}`, + ) + this.markBlock(test) + const lines = [] + const { instruction } = test + if (instruction.operation.kind !== 'branch') + decline( + 'unemittable-structured-control', + `Conditional region ${region.testBlockId} has a non-conditional leaf`, + ) + if ( + (region.regionExitBlockIds ?? []).some( + (entryId) => + !(context.regions ?? []).some((item) => item.entryId === entryId), + ) + ) + decline( + 'unemittable-structured-control', + `Conditional region ${region.testBlockId} has an unowned handler route`, + ) + // Every exit tail of a finally region jumps to its finalizer entry, so emitStructuredControl places the merge + // of a conditional whose arms leave the region there; for the region rule those arms meet at + // the statement's normal completion instead, and when the region has none they do not meet. + const finallyRegion = context.regions?.find( + (item) => item.kind === 'finally' && item.entryId === region.mergeBlockId, + ) + const merge = !finallyRegion ? region.mergeBlockId : finallyRegion.normalEnd + // An arm whose jump steps lead straight to the merge starts there, as the planner closes + // it: the steps run nothing. A back edge is a continue, never such a step. + const armStart = (pc) => { + const start = this.nodeForPc(pc).id + const steps = [] + let id = start + while (this.isJumpStep(this.nodeById.get(id)) && !steps.includes(id)) { + const next = this.outgoing(this.nodeById.get(id)) + if (next.length !== 1 || next[0].targetPc === null) break + if ( + this.loopTransfer(context, this.targetBlock(next[0]))?.kind === + 'continue' + ) + break + steps.push(id) + id = this.targetBlock(next[0]) + } + if (id !== merge) return start + for (const step of steps) this.markBlock(this.nodeById.get(step)) + return id + } + const branch = this.sequence( + armStart(region.branchTargetPc), + new Set([...stopIds, merge]), + merge === null + ? { ...context, terminalBlocks: new Set(region.branchTerminalBlockIds) } + : context, + ) + const fallthrough = this.sequence( + armStart(region.fallthroughTargetPc), + new Set([...stopIds, merge]), + merge === null + ? { + ...context, + terminalBlocks: new Set(region.fallthroughTerminalBlockIds), + } + : context, + ) + const nonTerminated = [branch, fallthrough].filter( + (path) => !path.terminated, + ) + const nextIds = new Set(nonTerminated.map(({ nextBlockId }) => nextBlockId)) + const mergeIsStructuredStop = + merge === null || + stopIds.has(merge) || + context.loops.some( + (loop) => loop.headerBlockId === merge || loop.exitTargets.has(merge), + ) + if ( + (nextIds.size > 1 && + ![...nextIds].every((nextBlockId) => nextBlockId === merge)) || + (nextIds.size === 1 && + ![...nextIds].every( + (nextBlockId) => nextBlockId === merge || stopIds.has(nextBlockId), + )) || + (!nonTerminated.length && !mergeIsStructuredStop) + ) + decline( + 'unemittable-structured-control', + `Conditional region ${region.testBlockId} does not reconverge at ${merge}`, + ) + const condition = registerName( + this.functionId, + instruction.operation.condition, + ) + const predicate = + instruction.operation.when === 'false' ? `!${condition}` : condition + if (branch.lines.length === 0 && fallthrough.lines.length === 0) { + // Both paths only reconverge; the condition has no recovered body. + } else if (branch.lines.length === 0) { + lines.push(`if (${invertPredicate(predicate)}) {`) + lines.push(...fallthrough.lines.map((line) => ` ${line}`)) + lines.push('}') + } else if (fallthrough.lines.length === 0) { + lines.push(`if (${predicate}) {`) + lines.push(...branch.lines.map((line) => ` ${line}`)) + lines.push('}') + } else { + lines.push(`if (${predicate}) {`) + lines.push(...branch.lines.map((line) => ` ${line}`)) + lines.push('} else {') + lines.push(...fallthrough.lines.map((line) => ` ${line}`)) + lines.push('}') + } + if (!nonTerminated.length) + return { + lines, + nextBlockId: merge, + terminated: true, + } + return { + lines, + nextBlockId: [...nextIds][0], + terminated: false, + } + } + + // Try statements are regions delimited by compiler-only head and tail instructions (see the + // try-region design): a catch-setup or finally-setup head, catch/finalizer entry heads, and + // exit tails made of handler-pop, an optional continuation literal and a jump. Every exit stays + // native: return and throw as written, any other transfer handed to the enclosing walk. + regionProtects(edge, context) { + const protector = context.regions?.at(-1) + if (!protector || edge.targetPc === null) return false + return ( + edge.kind === (protector.kind === 'catch' ? 'handler' : 'finally') && + this.targetBlock(edge) === protector.entryId + ) + } + + // Registers that only a region's own exit tails, pads and resume may touch: the continuation + // and payload of every enclosing finally region, plus those a finalizer walk bars for its own + // region. Native try/finally keeps the pending completion itself, so code that read or wrote + // them would behave differently once recovered. + carriers(context) { + const registers = new Set(context.barred ?? []) + for (const region of context.regions ?? []) + if (region.kind === 'finally') { + registers.add(region.continuation) + registers.add(region.payload) + } + return registers + } + + checkCarriers(instruction, context) { + const barred = this.carriers(context) + if (!barred.size) return + for (const register of functionRegisterSet([instruction])) + if (barred.has(register)) + decline( + 'unemittable-structured-control', + `Try region completion register used at ${instruction.pc}`, + ) + } + + // The handler records a block must carry on entry: one per enclosing region, innermost last. + // The solver builds one record per setup from its own fields, so the setup identifies it. + checkState(block, regions, pc = block.startPc) { + const stack = this.stateIn.get(block.startPc) + if (!stack) return + const same = + stack.length === regions.length && + regions.every((region, index) => stack[index].setupPc === region.setupPc) + if (!same) + decline( + 'unemittable-structured-control', + `Try region target state at ${pc} does not match its regions`, + ) + } + + // The block `block` falls into, through jump steps. + soleFallthrough(block) { + const edges = this.outgoing(block) + if ( + edges.length !== 1 || + edges[0].kind !== 'fallthrough' || + edges[0].targetPc === null + ) + return null + let next = this.nodeById.get(this.targetBlock(edges[0])) + const seen = new Set() + while (this.isJumpStep(next) && !seen.has(next.id)) { + seen.add(next.id) + const [edge] = this.outgoing(next) + if (edge?.targetPc == null) break + next = this.nodeById.get(this.targetBlock(edge)) + } + return next + } + + renderTry(block, context, stopIds) { + this.markBlock(block) + const setup = block.instruction + const { kind, target } = setup.operation + const entry = this.nodeForPc(target) + const bodyEdge = this.edgeTo(block, 'fallthrough') + if (!bodyEdge || bodyEdge.targetPc === null) + decline( + 'unemittable-structured-control', + `${kind} at ${setup.pc} has no body entry`, + ) + const region = { + kind: kind === 'catch-setup' ? 'catch' : 'finally', + setupPc: setup.pc, + entryId: entry.id, + outer: context, + } + const carriers = [setup.operation.continuation, setup.operation.payload] + if ( + region.kind === 'finally' && + carriers.some((register) => + localCaptureRegisterSet(this.instructions).has(register), + ) + ) + decline( + 'unemittable-structured-control', + `FINALLY_SETUP at ${setup.pc} has captured completion registers`, + ) + const exitTargets = new Set() + if (region.kind === 'catch') { + // The statement continues where the planner's row ends it: its normal end, or, when the + // block never completes normally, the frontend's layout end (where the source closed the + // statement); with neither, the catch keeps the code only it reaches. + const row = requireValue( + this.tryBySetup.get(block.id), + 'unemittable-structured-control', + `TRY_SETUP at ${setup.pc} has no planned try statement`, + ) + const end = row.continuationBlockId ?? row.layoutEndBlockId + if (end) exitTargets.add(end) + } else { + region.continuation = setup.operation.continuation + region.payload = setup.operation.payload + // The statement completes normally where its try row ends it; every other exit tail names + // a pad that re-issues an exit after the finalizer. + const row = requireValue( + this.tryBySetup.get(block.id), + 'unemittable-structured-control', + `FINALLY_SETUP at ${setup.pc} has no planned try statement`, + ) + region.normalEnd = row.continuationBlockId + if (region.normalEnd) exitTargets.add(region.normalEnd) + } + const stops = new Set([...stopIds, ...exitTargets]) + const body = this.sequence(this.targetBlock(bodyEdge), stops, { + ...context, + regions: [...(context.regions ?? []), region], + }) + if (!body.terminated && !exitTargets.has(body.nextBlockId)) + decline( + 'unemittable-structured-control', + `${kind} at ${setup.pc} body leaves its region unstructured`, + ) + let after = body.terminated ? null : body.nextBlockId + const lines = ['try {', ...body.lines.map((line) => ` ${line}`)] + if (region.kind === 'catch') { + const handler = this.sequence(entry.id, stops, context) + if (!handler.terminated) { + if ( + !exitTargets.has(handler.nextBlockId) || + (after !== null && handler.nextBlockId !== after) + ) + decline( + 'unemittable-structured-control', + `TRY_SETUP at ${setup.pc} catch does not rejoin its body`, + ) + after = handler.nextBlockId + } + lines.push('} catch (__recovered_error) {') + lines.push( + ` ${registerName(this.functionId, setup.operation.exceptionRegister)} = __recovered_error;`, + ) + lines.push(...handler.lines.map((line) => ` ${line}`)) + } else { + const resume = this.instructions.find( + (item) => + item.operation.kind === 'indirect-jump' && + item.operation.source === region.continuation && + this.reachesFrom(entry.id, this.nodeForPc(item.pc).id), + ) + if (!resume) + decline( + 'unemittable-structured-control', + `FINALLY_SETUP at ${setup.pc} has no finalizer resume`, + ) + const resumeBlock = this.nodeForPc(resume.pc) + const finalizer = this.sequence(entry.id, new Set([resumeBlock.id]), { + ...context, + barred: [...(context.barred ?? []), ...carriers], + }) + if (!finalizer.terminated && finalizer.nextBlockId !== resumeBlock.id) + decline( + 'unemittable-structured-control', + `FINALLY_SETUP at ${setup.pc} finalizer does not reach its resume`, + ) + if (finalizer.terminated) after = null + else { + this.markBlock(resumeBlock) + // The throw pad only rethrows the pending exception after the finalizer, which + // native try/finally already does. + const throwPad = this.nodeForPc(setup.operation.throwPad) + const { operation: pad } = throwPad.instruction + if (pad.kind !== 'throw' || pad.source !== region.payload) + decline( + 'unemittable-structured-control', + `FINALLY_SETUP at ${setup.pc} has no payload throw pad`, + ) + this.markBlock(throwPad) + } + lines.push('} finally {') + lines.push(...finalizer.lines.map((line) => ` ${line}`)) + } + lines.push('}') + return after !== null + ? { lines, nextBlockId: after, terminated: false } + : { lines, nextBlockId: null, terminated: true } + } + + // Whether `to` is reachable from `from` along normal edges. + reachesFrom(from, to) { + const seen = new Set() + const queue = [from] + while (queue.length) { + const id = queue.shift() + if (id === to) return true + if (seen.has(id)) continue + seen.add(id) + for (const edge of this.outgoing(this.nodeById.get(id))) + if ( + edge.targetPc !== null && + ['fallthrough', 'call', 'branch', 'conditional'].includes(edge.kind) + ) + queue.push(this.targetBlock(edge)) + } + return false + } + + // An exit tail of the innermost region, starting at `block`. A catch region's tail is its + // handler-pop; what follows belongs to the enclosing context. A finally region's tail is + // `continuation = literal` and handler-pop in either order, then the jump to the finalizer; + // the normal literal completes the statement, any other names a pad that re-issues the exit + // in the enclosing context, so the pad is walked here, in place. + renderRegionExit(block, region, context, stopIds) { + const lines = [] + // What follows this region's pop runs in the enclosing context; inside a pad it is still + // that pad. + // What follows a pop is no longer protected, yet it is rendered inside the native try + // block, so it may only transfer control (the compiler's unwind: further pops, routing, + // jumps, returns); inside a pad it is still that pad. + const outer = { + ...region.outer, + exiting: true, + ...(context.pad ? { pad: context.pad } : {}), + } + const outerRegions = region.outer.regions ?? [] + const checkPopEdge = (pop) => { + const after = this.solvedEdgesBySource + .get(pop.startPc) + ?.find(({ kind }) => kind === 'fallthrough')?.stackAfter + if ( + after && + (after.length !== outerRegions.length || + outerRegions.some( + (item, index) => after[index]?.setupPc !== item.setupPc, + )) + ) + decline( + 'unemittable-structured-control', + `Try region edge state at ${pop.startPc} does not leave exactly its region`, + ) + } + const state = this.stateIn.get(block.startPc) + if (region.kind === 'catch') { + const pop = block.instruction + if ( + pop.operation.kind !== 'handler-pop' || + (state && state.at(-1)?.setupPc !== region.setupPc) + ) + decline( + 'unemittable-structured-control', + `handler-pop at ${pop.pc} does not close TRY_SETUP at ${region.setupPc}`, + ) + this.markBlock(block) + checkPopEdge(block) + const next = this.edgeTo(block, 'fallthrough') + if (!next || next.targetPc === null) + decline( + 'unemittable-structured-control', + `handler-pop at ${pop.pc} has no continuation`, + ) + const rest = this.sequence(this.targetBlock(next), stopIds, outer) + return { ...rest, lines: [...lines, ...rest.lines] } + } + let current = block + let literal = null + let popped = false + for (;;) { + const leaf = current.instruction + if (popped) this.checkState(current, outerRegions) + else if (current !== block) + this.checkState(current, context.regions ?? []) + this.markBlock(current) + if (leaf.operation.kind === 'handler-pop' && !popped) { + popped = true + checkPopEdge(current) + } else if ( + leaf.operation.kind === 'assign' && + literal === null && + !popped && + current === block && + leaf.operation.destination === region.payload && + leaf.operation.value.kind === 'register' + ) + this.renderInstruction(leaf, lines) + else if ( + leaf.operation.kind === 'assign' && + literal === null && + leaf.operation.destination === region.continuation && + leaf.operation.value.kind === 'literal' + ) + literal = leaf.operation.value.value + else if ( + this.isJumpStep(current) && + this.nodeForPc(leaf.operation.target).id !== region.entryId + ) { + // A jump step between two steps of the tail. + } else if ( + leaf.operation.kind === 'jump' && + popped && + literal !== null && + this.nodeForPc(leaf.operation.target).id === region.entryId + ) + break + else + decline( + 'unemittable-structured-control', + `${leaf.operation.kind} at ${leaf.pc} breaks the exit tail of FINALLY_SETUP at ${region.setupPc}`, + ) + const next = this.outgoing(current) + if ( + next.length !== 1 || + next[0].kind !== + (this.isJumpStep(current) ? 'branch' : 'fallthrough') || + next[0].targetPc === null + ) + decline( + 'unemittable-structured-control', + `Exit tail of FINALLY_SETUP at ${region.setupPc} is not straight-line`, + ) + current = this.nodeById.get(this.targetBlock(next[0])) + } + if (this.nodeForPc(literal).id === region.normalEnd) + return { + lines, + nextBlockId: this.nodeForPc(literal).id, + terminated: false, + } + const pad = this.sequence(this.nodeForPc(literal).id, stopIds, { + ...region.outer, + pad: region, + exiting: true, + }) + return { ...pad, lines: [...lines, ...pad.lines] } + } + + // Whether `block` starts an exit tail of the innermost region. + startsRegionExit(block, context) { + const region = context.regions?.at(-1) + if (!region) return null + const leaf = block.instruction + if (leaf.operation.kind === 'handler-pop') return region + if ( + region.kind === 'finally' && + leaf.operation.kind === 'assign' && + leaf.operation.destination === region.continuation && + leaf.operation.value.kind === 'literal' + ) + return region + // A routed return first saves its value in the payload, then loads the continuation. + const next = this.soleFallthrough(block)?.instruction + if ( + region.kind === 'finally' && + leaf.operation.kind === 'assign' && + leaf.operation.destination === region.payload && + leaf.operation.value.kind === 'register' && + next?.operation.kind === 'assign' && + next.operation.destination === region.continuation && + next.operation.value.kind === 'literal' + ) + return region + return null + } + + renderForInNext(block, context) { + const loop = this.activeLoop(context) + if (!loop) + decline( + 'unemittable-structured-control', + `FOR_IN_NEXT at ${block.startPc} is not inside a proved natural loop`, + ) + const { instruction } = block + const outgoing = this.outgoing(block) + const branch = outgoing.find((edge) => edge.kind === 'branch') + const fallthrough = outgoing.find((edge) => edge.kind === 'fallthrough') + if (!branch || !fallthrough || branch.targetPc === null) + decline( + 'unemittable-structured-control', + `FOR_IN_NEXT at ${instruction.pc} has no natural loop exit`, + ) + const exit = this.targetBlock(branch) + if (!loop.exitTargets.has(exit)) + decline( + 'unemittable-structured-control', + `FOR_IN_NEXT at ${instruction.pc} exits outside the proved loop`, + ) + const iterator = registerName( + this.functionId, + instruction.operation.iterator, + ) + const destination = registerName( + this.functionId, + instruction.operation.destination, + ) + const lines = [ + `if (${iterator}.index >= ${iterator}.keys.length) break ${loop.label};`, + `${destination} = ${iterator}.keys[${iterator}.index++];`, + ] + return { + lines, + nextBlockId: this.targetBlock(fallthrough), + terminated: false, + } + } + + renderJump(block, context, stopIds, lines) { + const outgoing = this.outgoing(block) + const edge = outgoing.find(({ kind }) => kind === 'branch') + if ( + !edge || + edge.targetPc === null || + outgoing.some(({ kind }) => kind === 'finally') + ) + decline( + 'unemittable-structured-control', + `Unconditional jump at ${block.startPc} is not a direct structured edge`, + ) + const target = this.targetBlock(edge) + const transfer = this.loopTransfer(context, target) + if (transfer?.kind === 'continue') { + lines.push(`continue ${transfer.loop.label};`) + return { lines, nextBlockId: target, terminated: true } + } + if (transfer?.kind === 'break') { + lines.push(`break ${transfer.loop.label};`) + return { lines, nextBlockId: transfer.target, terminated: true } + } + if (stopIds.has(target)) + return { lines, nextBlockId: target, terminated: false } + // Any other jump is a step of the statement's own flow, as a fallthrough is: the walk + // continues at its target, and emitting each block once keeps anything shared out. + return { lines, nextBlockId: target, terminated: false } + } + + renderReturnOrThrow(block, instruction, lines, context) { + const outgoing = this.outgoing(block) + const edge = outgoing.find( + ({ kind }) => kind === 'return' || kind === 'throw', + ) + const handledThrow = + instruction.operation.kind === 'throw' && + outgoing.length === 1 && + this.regionProtects(outgoing[0], context) + if ( + !handledThrow && + (!edge || + edge.targetPc !== null || + outgoing.some(({ kind }) => kind === 'finally' || kind === 'handler')) + ) + decline( + 'unemittable-structured-control', + `${instruction.operation.kind} at ${instruction.pc} has an unstructured completion route`, + ) + if ( + context.pad && + (instruction.operation.kind !== 'return' || + instruction.operation.source !== context.pad.payload) + ) + decline( + 'unemittable-structured-control', + `Try region return payload at ${instruction.pc} is not the pending payload`, + ) + const source = registerName(this.functionId, instruction.operation.source) + lines.push(`${instruction.operation.kind} ${source};`) + return { lines, nextBlockId: null, terminated: true } + } + + renderOrdinary(block, instruction, lines, context) { + const outgoing = this.outgoing(block) + const normal = outgoing.filter( + ({ kind }) => kind === 'fallthrough' || kind === 'call', + ) + const exceptional = outgoing.filter(({ kind }) => + ['throw', 'handler', 'finally'].includes(kind), + ) + if ( + normal.length !== 1 || + normal[0].targetPc === null || + exceptional.some( + (edge) => + !( + (edge.kind === 'throw' && edge.targetPc === null) || + this.regionProtects(edge, context) + ), + ) + ) + decline( + 'unemittable-structured-control', + `${instruction.operation.kind} at ${instruction.pc} has an unstructured successor`, + ) + this.renderInstruction(instruction, lines) + return { + lines, + nextBlockId: this.targetBlock(normal[0]), + terminated: false, + } + } + + emitBlock(block, context, stopIds) { + this.checkState(block, context.regions ?? []) + const { instruction } = block + const terminal = instruction.operation.kind + if (terminal === 'catch-setup' || terminal === 'finally-setup') + return this.renderTry(block, context, stopIds) + const exiting = this.startsRegionExit(block, context) + if (exiting) return this.renderRegionExit(block, exiting, context, stopIds) + // A pad re-issues an exit after its finalizer; it is rendered at the exit, before the + // finalizer runs, so it may only transfer control and read its region's payload. + if (context.exiting && !['jump', 'return'].includes(terminal)) + decline( + 'unemittable-structured-control', + `Try region exit at ${block.startPc} is not a transfer`, + ) + this.checkCarriers(instruction, context) + this.markBlock(block) + const lines = [] + if (instruction.operation.kind === 'jump') + return this.renderJump(block, context, stopIds, lines) + if (['return', 'throw'].includes(instruction.operation.kind)) + return this.renderReturnOrThrow(block, instruction, lines, context) + if (instruction.operation.kind === 'branch') + decline( + 'unemittable-structured-control', + `Conditional block ${block.id} has no qualified conditional region`, + ) + if (instruction.operation.kind === 'indirect-jump') + decline( + 'unemittable-structured-control', + `JUMP_REG at ${instruction.pc} lacks a natural structured representation`, + ) + if (instruction.operation.kind === 'for-in-next') + return this.renderForInNext(block, context) + if ( + ['catch-setup', 'handler-pop', 'finally-setup'].includes( + instruction.operation.kind, + ) + ) + decline( + 'unemittable-structured-control', + `${instruction.operation.kind} at ${instruction.pc} requires structured exception syntax`, + ) + return this.renderOrdinary(block, instruction, lines, context) + } + + // A loop ends where its planned statement region continues; every other exit of the region is a + // transfer (break, continue, return or throw) its own block renders. + emitLoop(region, context) { + if (region.continuationBlockId === null) + decline( + 'unemittable-structured-control', + `Natural loop ${region.headerBlockId} has no continuation`, + ) + const parentLoops = context.loops + const exitTargets = new Set( + [region.continuationBlockId].filter( + (target) => !parentLoops.some((loop) => loop.exitTargets.has(target)), + ), + ) + if (exitTargets.size !== 1) + decline( + 'unemittable-structured-control', + `Natural loop ${region.headerBlockId} continues at an enclosing loop's exit`, + ) + const label = `__recovered_loop_${this.functionId}_${this.loopNumber++}` + const active = { + headerBlockId: region.headerBlockId, + continueBlockId: region.continueBlockId ?? null, + exitTargets, + label, + } + const body = this.sequence(region.headerBlockId, exitTargets, { + ...context, + loops: [...context.loops, active], + }) + if (!body.terminated && !exitTargets.has(body.nextBlockId)) + decline( + 'unemittable-structured-control', + `Natural loop ${region.headerBlockId} did not reach its proved exit`, + ) + if (region.testBlockIds) { + // The body's own end reaches the test as do-while's native flow does. + const bodyLines = + body.lines.at(-1) === `continue ${label};` + ? body.lines.slice(0, -1) + : body.lines + const lines = [`${label}: do {`, ...bodyLines.map((line) => ` ${line}`)] + if (!body.terminated) lines.push(` break ${label};`) + lines.push(`} while (${this.renderTest(region)});`) + return { lines, nextBlockId: [...exitTargets][0], terminated: false } + } + const lines = [ + active.continueBlockId === null + ? `${label}: while (true) {` + : `${label}: for (; ; ${this.renderUpdate(region)}) {`, + ] + lines.push(...body.lines.map((line) => ` ${line}`)) + if (!body.terminated) lines.push(` break ${label};`) + lines.push('}') + return { lines, nextBlockId: [...exitTargets][0], terminated: false } + } + + // A switch selects its clause natively: the head's leading operations run before it, each later + // case's run becomes its label, `(run…, value)`, which native evaluation runs in the chain's + // order until one matches, and the default is taken when none does. Each clause body is walked + // to the next clause's entry (a fallthrough), the end (a break) or a transfer; a body ending + // anywhere else declines. + emitSwitch(region, context) { + const head = this.nodeById.get(region.testBlockId) + this.markBlock(head) + const lines = [] + const owner = `Switch ${region.testBlockId}` + const labels = region.cases.map((item, index) => { + const value = registerName(this.functionId, item.value) + if (index === 0) return value + const test = this.nodeById.get(item.testBlockId) + const instructions = [] + for (const id of item.runBlockIds) { + const node = this.nodeById.get(id) + this.markBlock(node) + if (!this.isJumpStep(node)) instructions.push(node.instruction) + } + this.markBlock(test) + const parts = this.runExpressions(owner, instructions) + return parts.length ? `(${[...parts, value].join(', ')})` : value + }) + for (const id of [ + ...region.cases.flatMap(({ stepBlockIds }) => stepBlockIds), + ...region.defaultStepBlockIds, + ]) + this.markBlock(this.nodeById.get(id)) + const end = region.continuationBlockId + const label = `__recovered_switch_${this.functionId}_${this.switchNumber++}` + const active = { + kind: 'switch', + headerBlockId: null, + continueBlockId: null, + exitTargets: new Set(end === null ? [] : [end]), + label, + } + const entries = new Set( + region.clauses.map(({ entryBlockId }) => entryBlockId), + ) + const body = [] + region.clauses.forEach((clause, index) => { + body.push( + clause.caseIndex === null + ? 'default:' + : `case ${labels[clause.caseIndex]}:`, + ) + const following = region.clauses[index + 1]?.entryBlockId ?? null + // Grouped labels share the next clause's body. + if (following === clause.entryBlockId) return + const last = following === null + const walk = + clause.entryBlockId === end + ? { lines: [], nextBlockId: end, terminated: false } + : this.sequence( + clause.entryBlockId, + new Set( + [...entries, end].filter( + (id) => id !== null && id !== clause.entryBlockId, + ), + ), + { ...context, loops: [...context.loops, active] }, + ) + body.push(...walk.lines.map((line) => ` ${line}`)) + if (walk.terminated) return + if (walk.nextBlockId === end && end !== null) { + if (!last) body.push(` break ${label};`) + return + } + if (walk.nextBlockId !== following) + decline( + 'unemittable-structured-control', + `${owner} clause at ${clause.entryBlockId} does not reach the next clause or the end`, + ) + }) + lines.push( + `${label}: switch (${registerName(this.functionId, region.discriminant)}) {`, + ...body.map((line) => ` ${line}`), + '}', + ) + return end === null + ? { lines, nextBlockId: null, terminated: true } + : { lines, nextBlockId: end, terminated: false } + } + + // Rendered instructions as expressions; a statement cannot stand in a loop clause or a case + // label. `owner` names the statement for the refusal. + runExpressions(owner, instructions) { + const lines = [] + for (const instruction of instructions) + this.renderInstruction(instruction, lines) + return lines.map((line) => { + const [statement, ...rest] = parse(line, { + allowReturnOutsideFunction: true, + }).program.body + if (statement?.type !== 'ExpressionStatement' || rest.length) + decline( + 'unemittable-structured-control', + `${owner} clause is not an expression`, + ) + return line.slice(statement.expression.start, statement.expression.end) + }) + } + + // The run a loop's continue enters, as the clause a native continue runs: one comma expression + // from the continue target to `stop`, a conditional inside it (planned to close inside the + // run) written as `test ? (…) : (…)`. A for's update runs to the back edge's jump; a do-while's + // test to its own branch, whose condition the caller adds. + renderRun(region, runIds, stop) { + const updateIds = new Set(runIds) + const fail = (reason) => + decline( + 'unemittable-structured-control', + `Natural loop ${region.headerBlockId} continue run ${reason}`, + ) + const expressions = (instructions) => + this.runExpressions(`Natural loop ${region.headerBlockId}`, instructions) + const walk = (start, stop) => { + const parts = [] + let current = start + while (current !== stop) { + if (!updateIds.has(current)) fail(`leaves its run at ${current}`) + const block = this.nodeById.get(current) + this.markBlock(block) + const leaf = block.instruction + const conditional = this.conditionalByTest.get(current) + if (conditional) { + const condition = registerName( + this.functionId, + leaf.operation.condition, + ) + const predicate = + leaf.operation.when === 'false' ? `!${condition}` : condition + const arm = (pc) => + walk(this.nodeForPc(pc).id, conditional.mergeBlockId).join(', ') || + 'void 0' + parts.push( + `${predicate} ? (${arm(conditional.branchTargetPc)}) : (${arm(conditional.fallthroughTargetPc)})`, + ) + current = conditional.mergeBlockId + continue + } + const next = this.outgoing(block).filter( + ({ kind, targetPc }) => + targetPc !== null && + ['fallthrough', 'call', 'branch'].includes(kind), + ) + if (next.length !== 1) fail(`has no single next step at ${current}`) + if (!this.isJumpStep(block)) parts.push(...expressions([leaf])) + current = this.targetBlock(next[0]) + } + return parts + } + return walk(region.continueBlockId, stop) + } + + // A for's update clause. + renderUpdate(region) { + const parts = this.renderRun( + region, + region.updateBlockIds, + region.headerBlockId, + ) + if (region.updateBlockIds.some((id) => !this.emittedBlocks.has(id))) + decline( + 'unemittable-structured-control', + `Natural loop ${region.headerBlockId} update does not end at its back edge`, + ) + return parts.join(', ') + } + + // A do-while's test clause: the run up to its branch, then the condition under which the + // branch goes back to the header. + renderTest(region) { + const [test, latch] = region.testBlockIds.slice(-2) + const parts = this.renderRun(region, region.testBlockIds, test) + const testBlock = this.nodeById.get(test) + const latchBlock = this.nodeById.get(latch) + const branch = testBlock.instruction + const conditional = this.conditionalByTest.get(test) + const jump = latchBlock.instruction + if ( + !conditional || + jump.operation.kind !== 'jump' || + this.nodeForPc(jump.operation.target).id !== region.headerBlockId + ) + decline( + 'unemittable-structured-control', + `Natural loop ${region.headerBlockId} test does not end at its back edge`, + ) + this.markBlock(testBlock) + this.markBlock(latchBlock) + const condition = registerName(this.functionId, branch.operation.condition) + const predicate = + branch.operation.when === 'false' ? `!${condition}` : condition + const backOnBranch = this.nodeForPc(conditional.branchTargetPc).id === latch + parts.push(backOnBranch ? predicate : invertPredicate(predicate)) + return parts.length > 1 ? `(${parts.join(', ')})` : parts[0] + } + + sequence(startBlockId, stopIds, context) { + const lines = [] + let current = startBlockId + const visited = new Set() + while (current !== null && !stopIds.has(current)) { + if (visited.has(current)) + decline( + 'unemittable-structured-control', + `Structured path revisits block ${current} without a proved loop`, + ) + visited.add(current) + const loop = this.loopByHeader.get(current) + if (loop && this.activeLoop(context)?.headerBlockId !== current) { + const result = this.emitLoop(loop, context) + lines.push(...result.lines) + current = result.nextBlockId + continue + } + const transfer = + this.activeLoop(context)?.headerBlockId === current + ? null + : this.loopTransfer(context, current) + if (transfer?.kind === 'continue') { + const transferBlock = this.nodeById.get(current) + if (this.isJumpStep(transferBlock)) this.markBlock(transferBlock) + lines.push(`continue ${transfer.loop.label};`) + return { lines, nextBlockId: current, terminated: true } + } + if (transfer?.kind === 'break') { + const transferBlock = this.nodeById.get(current) + if (this.isJumpStep(transferBlock)) this.markBlock(transferBlock) + lines.push(`break ${transfer.loop.label};`) + return { lines, nextBlockId: transfer.target, terminated: true } + } + const switchRegion = this.switchByHead.get(current) + if (switchRegion) { + const result = this.emitSwitch(switchRegion, context) + lines.push(...result.lines) + if (result.terminated) + return { lines, nextBlockId: result.nextBlockId, terminated: true } + current = result.nextBlockId + continue + } + const conditional = this.conditionalByTest.get(current) + if (conditional) { + const result = this.renderConditional(conditional, context, stopIds) + lines.push(...result.lines) + if (result.terminated) + return { lines, nextBlockId: result.nextBlockId, terminated: true } + current = result.nextBlockId + continue + } + const block = this.nodeById.get(current) + requireValue( + block, + 'invalid-structured-control', + `Structured path refers to missing block ${current}`, + ) + const result = this.emitBlock(block, context, stopIds) + lines.push(...result.lines) + if (result.terminated) + return { lines, nextBlockId: result.nextBlockId, terminated: true } + current = result.nextBlockId + } + return { lines, nextBlockId: current, terminated: false } + } + + // Cases the walk did not emit are unreachable (a reachable one declines). They are rendered in + // runs, each in its own `if (false)`: a run ends after an operation that does not fall + // through, and a setup's handler, finalizer or throw pad starts one. + emitUnreachableBlocks(lines) { + const cases = [...this.instructions].sort( + (left, right) => left.pc - right.pc, + ) + const entries = new Set( + cases.flatMap(({ operation }) => + operation.kind === 'catch-setup' + ? [operation.target] + : operation.kind === 'finally-setup' + ? [operation.target, operation.throwPad] + : [], + ), + ) + const runs = [] + let run = null + for (const instruction of cases) { + const id = `${this.functionId}:b@${instruction.pc}` + if (this.emittedBlocks.has(id)) { + run = null + continue + } + if (this.reachablePcs.has(instruction.pc)) + decline( + 'unemittable-structured-control', + `Reachable structured block ${id} was not covered by qualified regions`, + ) + if (!run || entries.has(instruction.pc)) { + run = [] + runs.push(run) + } + run.push(instruction) + if (RUN_ENDING_KINDS.has(instruction.operation.kind)) run = null + } + for (const instructions of runs) { + lines.push('if (false) {') + for (const instruction of instructions) { + if (CONTROL_KINDS.has(instruction.operation.kind)) + lines.push(' void 0;') + else { + const rendered = [] + this.renderInstruction(instruction, rendered) + lines.push(...rendered.map((line) => ` ${line}`)) + } + } + lines.push('}') + for (const { pc } of instructions) + this.emittedBlocks.add(`${this.functionId}:b@${pc}`) + } + } + + emit() { + const entry = `${this.functionId}:b@${this.functionRecord.startPc}` + const result = this.sequence(entry, new Set(), { loops: [] }) + if (!result.terminated || result.nextBlockId !== null) + decline( + 'unemittable-structured-control', + `Structured function ${this.functionId} has no complete natural exit`, + ) + this.emitUnreachableBlocks(result.lines) + return result.lines + } +} + +export function emitStructuredFunction( + functionId, + instructions, + functionRecord, + regions, + functions, + solved, +) { + const lines = [] + const indent = (line) => ` ${line}` + const effective = effectiveStructuredInstructions( + functionId, + instructions, + solved, + ) + const registers = functionRegisterSet(effective.instructions) + const localCaptureRegisters = localCaptureRegisterSet(instructions) + const needsCaptures = functionRecord.captureCount > 0 + const needsArguments = functionNeedsArguments( + effective.instructions, + functionRecord, + ) + const parameters = [] + if (needsCaptures) parameters.push('__recovered_captures') + if (needsArguments) parameters.push('...__recovered_args') + lines.push(`function ${functionName(functionId)}(${parameters.join(', ')}) {`) + if (localCaptureRegisters.size > 0) + lines.push(indent(`const __recovered_cells_${functionId} = [];`)) + if (registers.size > 0) + lines.push( + indent( + `let ${[...registers] + .sort((left, right) => left - right) + .map((register) => registerName(functionId, register)) + .join(', ')};`, + ), + ) + if (functionId !== 0) { + if (needsArguments && functionRecord.hasRest) { + const restRegister = functionRecord.paramCount - 1 + for (let register = 0; register < restRegister; register++) { + if (!registers.has(register)) continue + lines.push( + indent( + `${registerName(functionId, register)} = __recovered_args[${register}];`, + ), + ) + } + if (registers.has(restRegister)) + lines.push( + indent( + `${registerName(functionId, restRegister)} = __recovered_args.slice(${restRegister});`, + ), + ) + } else if (needsArguments) { + for (let register = 0; register < functionRecord.paramCount; register++) { + if (!registers.has(register)) continue + lines.push( + indent( + `${registerName(functionId, register)} = __recovered_args[${register}];`, + ), + ) + } + } + if ( + needsArguments && + functionRecord.paramCount < functionRecord.regCount && + registers.has(functionRecord.paramCount) + ) + lines.push( + indent( + `${registerName(functionId, functionRecord.paramCount)} = __recovered_args;`, + ), + ) + } + const body = new StructuredFunctionEmitter( + functionId, + instructions, + functionRecord, + regions, + functions, + effective.copyPlan, + effective.coalescedMovePcs, + solved, + ).emit() + lines.push(...body.map(indent)) + lines.push('}') + return { lines, copyProofs: effective.copyProofs } +} diff --git a/src/vm/switch/vm-switch-to-source.js b/src/vm/switch/vm-switch-to-source.js new file mode 100644 index 00000000..953ba52e --- /dev/null +++ b/src/vm/switch/vm-switch-to-source.js @@ -0,0 +1,227 @@ +import { parse } from '@babel/parser' +import { + decline, + emitHandlerRegisterAssignment, + emitInstruction, + functionName, + functionNeedsArguments, + localCaptureRegisterSet, + registerName, +} from './vm-switch-copy-planning.js' +import { emitStructuredFunction } from './vm-switch-structured-emitter.js' +import { planStatements } from './vm-switch-statements.js' +import { solveControl } from './vm-switch-control.js' + +function emitStateMachineFunction( + functionId, + instructions, + functionRecord, + functions, +) { + const lines = [] + const indent = (line) => ` ${line}` + const needsCaptures = functionRecord.captureCount > 0 + const needsArguments = functionNeedsArguments(instructions, functionRecord) + const hasHandlerSetup = instructions.some(({ operation }) => + ['catch-setup', 'finally-setup'].includes(operation.kind), + ) + const usesHandlers = + hasHandlerSetup || + instructions.some(({ operation }) => operation.kind === 'handler-pop') + const parameters = [] + if (needsCaptures) parameters.push('__recovered_captures') + if (needsArguments) parameters.push('...__recovered_args') + lines.push(`function ${functionName(functionId)}(${parameters.join(', ')}) {`) + const localCaptureRegisters = localCaptureRegisterSet(instructions) + if (localCaptureRegisters.size > 0) + lines.push(indent(`const __recovered_cells_${functionId} = [];`)) + if (functionRecord.regCount > 0) + lines.push( + indent( + `let ${Array.from({ length: functionRecord.regCount }, (_, register) => registerName(functionId, register)).join(', ')};`, + ), + ) + if (usesHandlers) lines.push(indent('const __recovered_handlers = [];')) + if (functionId !== 0) { + if (needsArguments && functionRecord.hasRest) { + const restRegister = functionRecord.paramCount - 1 + for (let register = 0; register < restRegister; register++) + lines.push( + indent( + `${registerName(functionId, register)} = __recovered_args[${register}];`, + ), + ) + lines.push( + indent( + `${registerName(functionId, restRegister)} = __recovered_args.slice(${restRegister});`, + ), + ) + } else if (needsArguments) { + for (let register = 0; register < functionRecord.paramCount; register++) + lines.push( + indent( + `${registerName(functionId, register)} = __recovered_args[${register}];`, + ), + ) + } + if (needsArguments && functionRecord.paramCount < functionRecord.regCount) + lines.push( + indent( + `${registerName(functionId, functionRecord.paramCount)} = __recovered_args;`, + ), + ) + } + lines.push(indent(`let __recovered_pc = ${functionRecord.startPc};`)) + lines.push(indent('__recovered_dispatch: while (true) {')) + if (hasHandlerSetup) lines.push(indent(' try {')) + const switchIndent = hasHandlerSetup ? ' ' : ' ' + lines.push(indent(`${switchIndent}switch (__recovered_pc) {`)) + for (const instruction of instructions) { + const body = emitInstruction(functionId, instruction, functions) + lines.push(indent(`${switchIndent} case ${instruction.pc}: {`)) + for (const line of body.split('\n')) + lines.push(indent(`${switchIndent} ${line}`)) + lines.push(indent(`${switchIndent} }`)) + } + lines.push( + indent( + `${switchIndent} default: throw new Error('Invalid recovered control state');`, + ), + ) + lines.push(indent(`${switchIndent}}`)) + if (hasHandlerSetup) { + lines.push(indent(' } catch (__recovered_error) {')) + lines.push( + indent(' const __recovered_handler = __recovered_handlers.pop();'), + ) + lines.push(indent(' if (!__recovered_handler) throw __recovered_error;')) + lines.push(indent(" if (__recovered_handler.kind === 'catch') {")) + for (const line of emitHandlerRegisterAssignment( + functionId, + functionRecord.regCount, + 'register', + '__recovered_error', + )) + lines.push(indent(` ${line}`)) + lines.push(indent(' __recovered_pc = __recovered_handler.target;')) + lines.push(indent(' continue __recovered_dispatch;')) + lines.push(indent(' }')) + for (const line of emitHandlerRegisterAssignment( + functionId, + functionRecord.regCount, + 'continuation', + '__recovered_handler.throwPad', + )) + lines.push(indent(` ${line}`)) + for (const line of emitHandlerRegisterAssignment( + functionId, + functionRecord.regCount, + 'payload', + '__recovered_error', + )) + lines.push(indent(` ${line}`)) + lines.push(indent(' __recovered_pc = __recovered_handler.target;')) + lines.push(indent(' continue __recovered_dispatch;')) + lines.push(indent(' }')) + } + lines.push(indent('}')) + lines.push('}') + return lines +} + +export function emitVmSwitchProgram(model, { dispatch = false } = {}) { + const lines = ['(function () {', " 'use strict';"] + const functionSimplifications = [] + const statements = [] + const hasForIn = [...model.instructionsByFunction.values()].some( + (instructions) => + instructions.some(({ operation }) => operation.kind === 'for-in-setup'), + ) + if (hasForIn) + lines.push( + ' function __recovered_enumerable_keys(__value) {', + ' const __keys = [];', + ' if (__value === null || __value === void 0) return __keys;', + ' const __seen = Object.create(null);', + ' let __current = Object(__value);', + ' while (__current !== null) {', + ' for (const __key of Object.getOwnPropertyNames(__current)) {', + ' if (__key in __seen) continue;', + ' __seen[__key] = true;', + ' const __descriptor = Object.getOwnPropertyDescriptor(__current, __key);', + ' if (__descriptor && __descriptor.enumerable) __keys.push(__key);', + ' }', + ' __current = Object.getPrototypeOf(__current);', + ' }', + ' return __keys;', + ' }', + ) + // Every function is emitted structured, unless the caller asks for the review rendering, which + // emits every function as dispatch. + for (const functionRecord of model.functions.functions) { + const cases = model.instructionsByFunction.get(functionRecord.id) + const solved = dispatch ? null : solveControl(functionRecord, cases) + let emitted + if (solved) { + const regions = planStatements(functionRecord, cases, solved) + statements.push({ functionId: functionRecord.id, regions }) + emitted = emitStructuredFunction( + functionRecord.id, + cases, + functionRecord, + regions, + model.functions, + solved, + ) + } else + emitted = { + lines: emitStateMachineFunction( + functionRecord.id, + cases, + functionRecord, + model.functions, + ), + copyProofs: [], + } + lines.push(...emitted.lines.map((line) => ` ${line}`)) + functionSimplifications.push({ + functionId: functionRecord.id, + copies: emitted.copyProofs, + }) + } + const root = model.functions.functions[0] + const rootCaptures = root.captureCount > 0 ? '[]' : '' + lines.push(` ${functionName(0)}(${rootCaptures});`) + lines.push('})();') + const copies = functionSimplifications.flatMap(({ copies: proofs }) => proofs) + const output = lines.join('\n') + validateEmittedJavaScript(output) + return { + output, + statements, + simplification: { + schemaVersion: 'vm-switch-simplification.v1', + functions: functionSimplifications, + summary: { + emitted: model.instructionCount - copies.length, + safelyEliminated: copies.length, + provenUnreachable: 0, + }, + }, + } +} + +function validateEmittedJavaScript(source) { + try { + parse(source, { + allowReturnOutsideFunction: true, + errorRecovery: false, + sourceType: 'script', + }) + } catch (error) { + decline( + 'emitted-parse-failed', + `Fresh parse of VM switch output failed: ${error.message}`, + ) + } +} diff --git a/test/vm/switch/vm-switch-to-source.test.js b/test/vm/switch/vm-switch-to-source.test.js new file mode 100644 index 00000000..070ffd12 --- /dev/null +++ b/test/vm/switch/vm-switch-to-source.test.js @@ -0,0 +1,305 @@ +import { test, expect } from 'vitest' +import { runInNewContext } from 'node:vm' +import { emitVmSwitchProgram } from '../../../src/vm/switch/vm-switch-to-source.js' + +function runSource(source, context) { + return runInNewContext(source, context, { timeout: 1000 }) +} + +function sourceSwitchModel() { + const cases = [ + { + pc: 0, + nextPc: 1, + operation: { + kind: 'assign', + destination: 0, + value: { kind: 'literal', value: 7 }, + }, + }, + { + pc: 1, + nextPc: 2, + operation: { kind: 'global-write', name: 'RESULT', source: 0 }, + }, + { pc: 2, nextPc: 3, operation: { kind: 'return', source: 0 } }, + ] + return { + schemaVersion: 'decode-js-vm-switch.v1', + storage: { + kind: 'register-machine', + functions: [{ id: 0, registerCount: 1, captureCount: 0 }], + }, + functions: { + functions: [ + { + id: 0, + startPc: 0, + regCount: 1, + paramCount: 0, + captureCount: 0, + hasRest: false, + }, + ], + }, + instructionsByFunction: new Map([[0, cases]]), + instructionCount: 3, + } +} + +function withCases(cases, registerCount) { + const model = sourceSwitchModel() + model.storage.functions[0].registerCount = registerCount + model.functions.functions[0].regCount = registerCount + model.instructionsByFunction.set(0, cases) + model.instructionCount = cases.length + return model +} + +test.each(['structured', 'dispatch'])( + '%s: DEBUGGER emits no statement and preserves the following effect', + (mode) => { + const operations = [ + { kind: 'assign', destination: 0, value: { kind: 'literal', value: 7 } }, + { kind: 'debugger' }, + { kind: 'global-write', name: 'RESULT', source: 0 }, + { kind: 'return', source: 0 }, + ] + const model = withCases( + operations.map((operation, pc) => ({ pc, nextPc: pc + 1, operation })), + 1, + ) + const { output } = emitVmSwitchProgram(model, { + dispatch: mode === 'dispatch', + }) + expect(output).not.toContain('debugger;') + expect(output).not.toContain('void 0;') + expect(output.includes('__recovered_dispatch')).toBe(mode === 'dispatch') + const context = { RESULT: null } + context.globalThis = context + runSource(output, context) + expect(context.RESULT).toBe(7) + }, +) + +// One case per operation, each falling through to the next PC. +function casesOf(operations, registerCount) { + return withCases( + operations.map((operation, pc) => ({ pc, nextPc: pc + 1, operation })), + registerCount, + ) +} + +// Run the program; return RESULT, the copy proofs and the declared and used registers. +function emitCopies(model) { + const { output, simplification } = emitVmSwitchProgram(model) + const context = { RESULT: null } + context.globalThis = context + runSource(output, context) + const [declaration, ...body] = output + .split('\n') + .filter((line) => line.includes('__recovered_r_0_')) + const names = (lines) => + [...new Set(lines.join('\n').match(/__recovered_r_0_\d+/g) ?? [])].sort() + return { + output, + result: context.RESULT, + copies: simplification.functions[0].copies, + declared: names([declaration]), + used: names(body), + } +} + +// var w = 140; var d = w; (js-confuser-vm default options: LOAD_CONST, MOVE w, MOVE d = w). +// Both moves coalesce: the second move's producer is the first move, so the chain resolves to +// one write of the final register at the retained root. +const chain = [ + { kind: 'assign', destination: 0, value: { kind: 'literal', value: 140 } }, + { kind: 'assign', destination: 1, value: { kind: 'register', index: 0 } }, + { kind: 'assign', destination: 2, value: { kind: 'register', index: 1 } }, + { kind: 'global-write', name: 'RESULT', source: 2 }, + { kind: 'return', source: 2 }, +] +const chainRows = (root, links) => + links.map(([producerPc, movePc, sourceRegister]) => ({ + producerPc: root, + movePc, + sourceRegister, + destinationRegister: 2, + reachingDefinitions: [String(producerPc)], + })) +const row = ({ + producerPc, + movePc, + sourceRegister, + destinationRegister, + reachingDefinitions, +}) => ({ + producerPc, + movePc, + sourceRegister, + destinationRegister, + reachingDefinitions, +}) + +test.each([ + [ + 'across blocks', + chain, + 0, + [ + [0, 1, 0], + [1, 2, 1], + ], + ], + // The root is laid out after its move and reaches it through a jump step, so the later + // link (1 -> 2) is planned before the earlier one (5 -> 1), as in VM-1 vm-full. + [ + 'in reverse visit order through jump steps', + [ + { kind: 'jump', target: 5 }, + chain[1], + chain[2], + chain[3], + chain[4], + chain[0], + { kind: 'jump', target: 1 }, + ], + 5, + [ + [1, 2, 1], + [5, 1, 0], + ], + ], +])( + 'a copy chain %s resolves to one write of its final register', + (_, operations, root, links) => { + const emitted = emitCopies(casesOf(operations, 3)) + expect(emitted.result).toBe(140) + expect(emitted.copies.map(row)).toEqual(chainRows(root, links)) + const removed = new Set(emitted.copies.map(({ movePc }) => movePc)) + expect( + emitted.copies.some(({ producerPc }) => removed.has(producerPc)), + ).toBe(false) + expect(emitted.output).toContain('__recovered_r_0_2 = 140;') + expect(emitted.declared).toEqual(emitted.used) + expect(emitted.declared).not.toContain('__recovered_r_0_1') + }, +) + +test('a single copy still coalesces into its producer', () => { + const operations = [ + { kind: 'assign', destination: 0, value: { kind: 'literal', value: 140 } }, + { kind: 'assign', destination: 1, value: { kind: 'register', index: 0 } }, + { kind: 'global-write', name: 'RESULT', source: 1 }, + { kind: 'return', source: 1 }, + ] + const emitted = emitCopies(casesOf(operations, 2)) + expect(emitted.result).toBe(140) + expect(emitted.copies.map(row)).toEqual([ + { + producerPc: 0, + movePc: 1, + sourceRegister: 0, + destinationRegister: 1, + reachingDefinitions: ['0'], + }, + ]) + expect(emitted.output).toContain('__recovered_r_0_1 = 140;') + expect(emitted.declared).toEqual(emitted.used) +}) + +test('declines a throw after handler pop, which is not a transfer', () => { + const operations = [ + { kind: 'catch-setup', target: 4, exceptionRegister: 0 }, + { kind: 'assign', destination: 1, value: { kind: 'literal', value: 7 } }, + { kind: 'handler-pop' }, + { kind: 'throw', source: 1 }, + { kind: 'global-write', name: 'CAUGHT', source: 0 }, + { kind: 'return', source: 0 }, + ] + const model = casesOf(operations, 2) + // After TRY_END the code is no longer protected but would sit inside the native try + // block; the compiler only places transfers there, so anything else declines. + expect(() => emitVmSwitchProgram(model)).toThrowError(/is not a transfer/) +}) + +test('declines an effect after handler pop, which is not a transfer', () => { + const operations = [ + { kind: 'assign', destination: 0, value: { kind: 'literal', value: 9 } }, + { kind: 'catch-setup', target: 6, exceptionRegister: 1 }, + { kind: 'assign', destination: 2, value: { kind: 'literal', value: 7 } }, + { kind: 'handler-pop' }, + { kind: 'global-write', name: 'RESULT', source: 0 }, + { kind: 'return', source: 2 }, + { kind: 'return', source: 1 }, + ] + const model = casesOf(operations, 3) + // After TRY_END the code is no longer protected but would sit inside the native try + // block; the compiler only places transfers there, so anything else declines. + expect(() => emitVmSwitchProgram(model)).toThrowError(/is not a transfer/) +}) + +test('declines normal-completion work after handler pop', () => { + const operations = [ + { kind: 'assign', destination: 0, value: { kind: 'literal', value: 9 } }, + { kind: 'catch-setup', target: 6, exceptionRegister: 1 }, + { kind: 'global-write', name: 'BODY', source: 0 }, + { kind: 'handler-pop' }, + { kind: 'global-write', name: 'NORMAL', source: 0 }, + { kind: 'jump', target: 7 }, + { kind: 'global-write', name: 'CAUGHT', source: 1 }, + { kind: 'global-write', name: 'RESULT', source: 0 }, + { kind: 'return', source: 0 }, + ] + const model = casesOf(operations, 2) + // The normal completion's work between TRY_END and its jump is not compiler grammar. + expect(() => emitVmSwitchProgram(model)).toThrowError(/is not a transfer/) +}) + +test('emits a source-switch model without numeric wordcode', () => { + const model = sourceSwitchModel() + const { output } = emitVmSwitchProgram(model) + const context = { RESULT: null } + context.globalThis = context + runSource(output, context) + expect(context.RESULT).toBe(7) + expect(output).not.toContain('BYTECODE') + expect(output).not.toContain('__recovered_dispatch') +}) + +test('retains a catch dispatch when a typed case installs a handler', () => { + const model = withCases( + [ + { + pc: 0, + nextPc: 1, + operation: { kind: 'catch-setup', target: 3, exceptionRegister: 0 }, + }, + { + pc: 1, + nextPc: 2, + operation: { + kind: 'assign', + destination: 1, + value: { kind: 'literal', value: 'boom' }, + }, + }, + { pc: 2, nextPc: 3, operation: { kind: 'throw', source: 1 } }, + { + pc: 3, + nextPc: 4, + operation: { kind: 'global-write', name: 'RESULT', source: 0 }, + }, + { pc: 4, nextPc: 5, operation: { kind: 'return', source: 0 } }, + ], + 2, + ) + const { output } = emitVmSwitchProgram(model, { dispatch: true }) + const context = { RESULT: null } + context.globalThis = context + runSource(output, context) + expect(context.RESULT).toBe('boom') + expect(output).toContain('catch (__recovered_error)') + expect(output).toContain('switch (__recovered_handler.register)') +}) From 42e49f4c9c19ea4a7262a9ab6949bf1f9bee658f Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 10/12] feat(decode-js): decode standalone VM programs through the switch backend Joins the jsconfuser-vm frontend to the shared backend: the diagnosed program is lowered into the switch model and rendered. The tests run real encoder output end to end, so each component is judged by the decoded program's behaviour against its source. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- src/vm/jsconfuser-vm/decode-standalone.js | 121 + src/vm/jsconfuser-vm/numeric-to-vm-switch.js | 306 + .../analyze-anti-runtime.test.js | 253 + .../class-runtime-combinations.test.js | 52 + test/vm/jsconfuser-vm/control-solver.test.js | 136 + .../jsconfuser-vm/decode-standalone.test.js | 522 ++ .../encoded-bytecode-combinations.test.js | 63 + .../fixtures/aliased-runtime-boot/encoded.js | 1135 ++++ .../aliased-runtime-boot/qualification.json | 57 + .../fixtures/aliased-runtime-boot/source.js | 1 + .../aliased-runtime-closures/encoded.js | 1142 ++++ .../qualification.json | 63 + .../aliased-runtime-closures/source.js | 1 + .../aliased-runtime-exceptions/encoded.js | 1218 ++++ .../qualification.json | 65 + .../aliased-runtime-exceptions/source.js | 1 + .../fixtures/anti-alias-exceptions/encoded.js | 1285 ++++ .../anti-alias-exceptions/qualification.json | 73 + .../fixtures/anti-alias-exceptions/source.js | 1 + .../fixtures/anti-macro-exceptions/encoded.js | 1279 ++++ .../anti-macro-exceptions/qualification.json | 73 + .../fixtures/anti-macro-exceptions/source.js | 1 + .../anti-runtime-catch-finally/encoded.js | 1134 ++++ .../qualification.json | 69 + .../anti-runtime-catch-finally/source.js | 1 + .../fixtures/anti-runtime-finally/encoded.js | 1070 ++++ .../anti-runtime-finally/qualification.json | 66 + .../fixtures/anti-runtime-finally/source.js | 1 + .../anti-specialized-exceptions/encoded.js | 1465 +++++ .../qualification.json | 73 + .../anti-specialized-exceptions/source.js | 1 + .../encoded.js | 1179 ++++ .../qualification.json | 73 + .../source.js | 1 + .../fixtures/cff-branches/encoded.js | 1019 ++++ .../fixtures/cff-branches/qualification.json | 57 + .../fixtures/cff-branches/source.js | 1 + .../cff-dispatcher-try-finally/encoded.js | 1019 ++++ .../qualification.json | 70 + .../cff-dispatcher-try-finally/source.js | 1 + .../fixtures/cff-exceptions/encoded.js | 1019 ++++ .../cff-exceptions/qualification.json | 69 + .../fixtures/cff-exceptions/source.js | 1 + .../fixtures/cff-for-in/encoded.js | 1019 ++++ .../fixtures/cff-for-in/qualification.json | 57 + .../fixtures/cff-for-in/source.js | 1 + .../fixtures/cff-loop-return/encoded.js | 1019 ++++ .../fixtures/cff-loop-return/generation.json | 12 + .../fixtures/cff-loop-return/plain.encoded.js | 1019 ++++ .../fixtures/cff-loop-return/source.js | 6 + .../fixtures/cff-switch/encoded.js | 1019 ++++ .../fixtures/cff-switch/qualification.json | 66 + .../fixtures/cff-switch/source.js | 1 + .../fixtures/cff-try-catch/encoded.js | 1019 ++++ .../fixtures/cff-try-catch/qualification.json | 66 + .../fixtures/cff-try-catch/source.js | 1 + .../fixtures/cff-try-finally/encoded.js | 1019 ++++ .../cff-try-finally/qualification.json | 66 + .../fixtures/cff-try-finally/source.js | 1 + .../encoded.js | 2500 ++++++++ .../qualification.json | 82 + .../source.js | 1 + .../fixtures/class-runtime-boot/encoded.js | 959 +++ .../class-runtime-boot/qualification.json | 58 + .../fixtures/class-runtime-boot/source.js | 1 + .../encoded.js | 1108 ++++ .../qualification.json | 73 + .../source.js | 1 + .../fixtures/dead-code/encoded.js | 1019 ++++ .../fixtures/dead-code/generation.json | 11 + .../fixtures/dead-code/source.js | 13 + .../encoded.js | 1355 +++++ .../qualification.json | 70 + .../source.js | 1 + .../dispatcher-array-result/encoded.js | 1019 ++++ .../qualification.json | 57 + .../dispatcher-array-result/source.js | 1 + .../fixtures/dispatcher-branches/encoded.js | 1019 ++++ .../dispatcher-branches/qualification.json | 57 + .../fixtures/dispatcher-branches/source.js | 1 + .../fixtures/dispatcher-exceptions/encoded.js | 1019 ++++ .../dispatcher-exceptions/qualification.json | 69 + .../fixtures/dispatcher-exceptions/source.js | 1 + .../fixtures/dispatcher-finally/encoded.js | 1019 ++++ .../dispatcher-finally/qualification.json | 66 + .../fixtures/dispatcher-finally/source.js | 1 + .../dispatcher-object-result/encoded.js | 1019 ++++ .../qualification.json | 57 + .../dispatcher-object-result/source.js | 1 + .../fixtures/dispatcher-split-key/encoded.js | 1019 ++++ .../dispatcher-split-key/qualification.json | 57 + .../fixtures/dispatcher-split-key/source.js | 1 + .../fixtures/dispatcher-switch/encoded.js | 1019 ++++ .../dispatcher-switch/qualification.json | 66 + .../fixtures/dispatcher-switch/source.js | 1 + .../eight-option-alias-exceptions/encoded.js | 2460 ++++++++ .../qualification.json | 79 + .../eight-option-alias-exceptions/source.js | 1 + .../encoded-class-stack-exceptions/encoded.js | 2371 ++++++++ .../qualification.json | 83 + .../encoded-class-stack-exceptions/source.js | 1 + .../fixtures/handler-table-boot/encoded.js | 1028 ++++ .../handler-table-boot/qualification.json | 57 + .../fixtures/handler-table-boot/source.js | 1 + .../fixtures/legacy-common/common.js | 1 + .../loop-sharing-cff-prefix/encoded.js | 1019 ++++ .../qualification.json | 58 + .../loop-sharing-cff-prefix/source.js | 10 + .../macro-alias-exceptions/encoded.js | 1317 +++++ .../macro-alias-exceptions/qualification.json | 73 + .../fixtures/macro-alias-exceptions/source.js | 1 + .../fixtures/macro-runtime-arrays/encoded.js | 1323 +++++ .../macro-runtime-arrays/qualification.json | 96 + .../fixtures/macro-runtime-arrays/source.js | 1 + .../fixtures/macro-runtime-boot/encoded.js | 1030 ++++ .../macro-runtime-boot/qualification.json | 57 + .../fixtures/macro-runtime-boot/source.js | 1 + .../macro-runtime-branches/encoded.js | 1077 ++++ .../macro-runtime-branches/qualification.json | 57 + .../fixtures/macro-runtime-branches/source.js | 1 + .../macro-runtime-closures/encoded.js | 1088 ++++ .../macro-runtime-closures/qualification.json | 66 + .../fixtures/macro-runtime-closures/source.js | 1 + .../macro-runtime-exceptions/encoded.js | 1131 ++++ .../qualification.json | 69 + .../macro-runtime-exceptions/source.js | 1 + .../macro-seven-option-exceptions/encoded.js | 2353 ++++++++ .../qualification.json | 78 + .../macro-seven-option-exceptions/source.js | 1 + .../encoded.js | 2258 +++++++ .../qualification.json | 105 + .../source.js | 1 + .../encoded.js | 5266 +++++++++++++++++ .../qualification.json | 77 + .../source.js | 1 + .../nested-strings/encoded-169.js | 16 + .../nested-strings/encoded-170.js | 17 + .../nested-strings/encoded-171.js | 16 + .../nested-strings/source.js | 1 + .../encoded.js | 17 + .../qualification.json | 74 + .../source.js | 1 + .../dead-code/encoded.js | 1019 ++++ .../dead-code/generation.json | 12 + .../dead-code/plain.encoded.js | 1019 ++++ .../dead-code/source.js | 20 + .../fixtures/patch-added-opcodes/alias.js | 1135 ++++ .../fixtures/patch-added-opcodes/alias.src.js | 1 + .../fixtures/patch-added-opcodes/anti.js | 1087 ++++ .../fixtures/patch-added-opcodes/anti.src.js | 1 + .../fixtures/patch-added-opcodes/arrays.js | 1360 +++++ .../patch-added-opcodes/arrays.src.js | 1 + .../fixtures/patch-added-opcodes/finally.js | 1325 +++++ .../patch-added-opcodes/finally.src.js | 1 + .../patch-added-opcodes/generation.json | 32 + .../fixtures/patch-added-opcodes/macro.js | 1030 ++++ .../fixtures/patch-added-opcodes/macro.src.js | 1 + .../patch-added-opcodes/specialized.js | 1140 ++++ .../patch-added-opcodes/specialized.src.js | 1 + .../fixtures/patch-added-opcodes/stack.js | 1204 ++++ .../fixtures/patch-added-opcodes/stack.src.js | 1 + .../readability/arithmetic/decoded.js | 21 + .../readability/arithmetic/encoded.js | 1035 ++++ .../fixtures/readability/arithmetic/source.js | 5 + .../fixtures/readability/baseline.json | 116 + .../readability/closure-loop/decoded.js | 59 + .../readability/closure-loop/encoded.js | 1062 ++++ .../readability/closure-loop/source.js | 12 + .../fixtures/readability/manifest.json | 29 + .../fixtures/readability/scale/decoded.js | 209 + .../fixtures/readability/scale/encoded.js | 1220 ++++ .../fixtures/readability/scale/source.js | 98 + ...rithmetic-alias-specialized-212.encoded.js | 1149 ++++ .../self-modifying-layers/arithmetic.src.js | 1 + .../cff-anti-2.encoded.js | 1168 ++++ .../dispatcher-anti-2.encoded.js | 1336 +++++ .../dispatcher-anti-7.encoded.js | 1343 +++++ ...-alias-specialized-dispatcher-1.encoded.js | 1662 ++++++ .../finalizers-dispatcher-1.encoded.js | 1019 ++++ .../finalizers-dispatcher-2.encoded.js | 1019 ++++ .../finalizers-dispatcher-4.encoded.js | 1019 ++++ .../finalizers-patch-232.encoded.js | 1549 +++++ .../self-modifying-layers/finalizers.src.js | 1 + .../self-modifying-layers/generation.json | 108 + .../loops-dispatcher-2.encoded.js | 1019 ++++ .../self-modifying-layers/loops.src.js | 18 + .../patch-arrays-full-213.encoded.js | 1262 ++++ .../self-modifying-layers/patch-arrays.src.js | 1 + .../fixtures/self-modifying-layers/source.js | 2 + .../encoded.js | 2653 +++++++++ .../qualification.json | 80 + .../shuffled-alias-stack-exceptions/source.js | 1 + .../encoded.js | 1334 +++++ .../qualification.json | 61 + .../source.js | 1 + .../specialized-runtime-boot/encoded.js | 1140 ++++ .../qualification.json | 57 + .../specialized-runtime-boot/source.js | 1 + .../specialized-runtime-branches/encoded.js | 1212 ++++ .../qualification.json | 57 + .../specialized-runtime-branches/source.js | 1 + .../specialized-runtime-closures/encoded.js | 1283 ++++ .../qualification.json | 66 + .../specialized-runtime-closures/source.js | 1 + .../specialized-runtime-exceptions/encoded.js | 1401 +++++ .../qualification.json | 69 + .../specialized-runtime-exceptions/source.js | 1 + .../encoded.js | 1436 +++++ .../qualification.json | 71 + .../source.js | 1 + .../encoded.js | 1392 +++++ .../qualification.json | 76 + .../source.js | 1 + .../encoded.js | 1856 ++++++ .../qualification.json | 77 + .../source.js | 1 + .../encoded.js | 1809 ++++++ .../qualification.json | 64 + .../source.js | 1 + .../encoded.js | 1206 ++++ .../qualification.json | 61 + .../source.js | 1 + .../encoded.js | 1019 ++++ .../qualification.json | 75 + .../source.js | 1 + .../encoded.js | 1019 ++++ .../qualification.json | 74 + .../source.js | 1 + .../encoded.js | 1019 ++++ .../qualification.json | 71 + .../source.js | 1 + .../encoded.js | 1019 ++++ .../qualification.json | 83 + .../source.js | 3 + .../encoded.js | 1019 ++++ .../qualification.json | 68 + .../source.js | 1 + .../fixtures/string-concealing-cff/encoded.js | 1019 ++++ .../string-concealing-cff/qualification.json | 70 + .../fixtures/string-concealing-cff/source.js | 1 + .../string-concealing-closures/encoded.js | 1019 ++++ .../qualification.json | 66 + .../string-concealing-closures/source.js | 1 + .../encoded.js | 1019 ++++ .../qualification.json | 70 + .../source.js | 1 + .../string-concealing-dispatcher/encoded.js | 1019 ++++ .../qualification.json | 70 + .../string-concealing-dispatcher/source.js | 1 + .../string-concealing-exceptions/encoded.js | 1019 ++++ .../qualification.json | 69 + .../string-concealing-exceptions/source.js | 1 + .../string-concealing-methods/encoded.js | 1019 ++++ .../qualification.json | 57 + .../string-concealing-methods/source.js | 1 + .../string-concealing-nested-cff/encoded.js | 1019 ++++ .../qualification.json | 61 + .../string-concealing-nested-cff/source.js | 1 + .../string-concealing-nested/encoded.js | 1019 ++++ .../qualification.json | 57 + .../string-concealing-nested/source.js | 1 + .../string-concealing-switch/encoded.js | 1019 ++++ .../qualification.json | 66 + .../string-concealing-switch/source.js | 1 + test/vm/jsconfuser-vm/integration/harness.js | 690 +++ .../integration/readability-metrics.js | 376 ++ .../integration/readability.test.js | 196 + .../minified-self-modifying-encoded.test.js | 65 + .../normalize-alias-combinations.test.js | 230 + .../normalize-aliased-wordcode.test.js | 109 + ...normalize-anti-concealed-constants.test.js | 77 + ...ormalize-anti-routing-combinations.test.js | 146 + ...lize-anti-specialized-combinations.test.js | 140 + .../normalize-anti-wordcode.test.js | 159 + .../normalize-cff-wordcode.test.js | 221 + .../normalize-dispatcher-wordcode.test.js | 238 + .../normalize-macro-combinations.test.js | 163 + .../normalize-macro-wordcode.test.js | 181 + .../normalize-self-modifying-wordcode.test.js | 138 + .../normalize-specialized-wordcode.test.js | 98 + ...ize-string-concealing-combinations.test.js | 298 + .../normalize-string-concealing.test.js | 151 + .../numeric-to-vm-switch.test.js | 27 + .../jsconfuser-vm/owned-chain-source.test.js | 203 + .../vm-copy-simplification.test.js | 144 + .../exits/forms/encoded.js | 1019 ++++ .../exits/forms/source.js | 18 + .../exits/generation.json | 11 + .../loop-tests/forms/encoded.js | 1019 ++++ .../loop-tests/forms/source.js | 8 + .../loop-tests/generation.json | 11 + .../nested-loops/forms/encoded.js | 1019 ++++ .../nested-loops/forms/source.js | 10 + .../nested-loops/generation.json | 11 + .../try-ends/dowhile-continue/encoded.js | 1019 ++++ .../try-ends/dowhile-continue/source.js | 3 + .../try-ends/forms-cff/encoded.js | 1019 ++++ .../try-ends/forms-cff/source.js | 19 + .../try-ends/forms/encoded.js | 1019 ++++ .../try-ends/forms/source.js | 19 + .../try-ends/generation.json | 29 + .../try-ends/switch-break-cff/encoded.js | 1019 ++++ .../try-ends/switch-break-cff/source.js | 3 + .../try-ends/switch-break/encoded.js | 1019 ++++ .../try-ends/switch-break/source.js | 3 + .../try-region/forms-cff/encoded.js | 1019 ++++ .../try-region/forms-cff/source.js | 32 + .../try-region/forms/encoded.js | 1019 ++++ .../try-region/forms/source.js | 32 + .../try-region/generation.json | 21 + .../try-region/routes/encoded.js | 1019 ++++ .../try-region/routes/source.js | 39 + test/vm/switch/vm-switch-statements.test.js | 477 ++ 313 files changed, 138938 insertions(+) create mode 100644 src/vm/jsconfuser-vm/decode-standalone.js create mode 100644 src/vm/jsconfuser-vm/numeric-to-vm-switch.js create mode 100644 test/vm/jsconfuser-vm/analyze-anti-runtime.test.js create mode 100644 test/vm/jsconfuser-vm/class-runtime-combinations.test.js create mode 100644 test/vm/jsconfuser-vm/control-solver.test.js create mode 100644 test/vm/jsconfuser-vm/decode-standalone.test.js create mode 100644 test/vm/jsconfuser-vm/encoded-bytecode-combinations.test.js create mode 100644 test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-branches/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-branches/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-branches/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-for-in/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-for-in/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-for-in/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-loop-return/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-loop-return/generation.json create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-loop-return/plain.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-loop-return/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-switch/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-switch/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-switch/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-try-catch/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-try-catch/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-try-catch/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-try-finally/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-try-finally/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/cff-try-finally/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/class-runtime-boot/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/class-runtime-boot/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/class-runtime-boot/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dead-code/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dead-code/generation.json create mode 100644 test/vm/jsconfuser-vm/fixtures/dead-code/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-branches/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-branches/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-branches/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-finally/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-finally/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-finally/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-switch/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-switch/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/dispatcher-switch/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/handler-table-boot/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/handler-table-boot/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/handler-table-boot/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/legacy-common/common.js create mode 100644 test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-169.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-170.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-171.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/generation.json create mode 100644 test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/plain.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/alias.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/alias.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/anti.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/anti.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/arrays.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/arrays.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/finally.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/finally.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/generation.json create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/macro.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/macro.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/specialized.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/specialized.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/stack.js create mode 100644 test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/stack.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/arithmetic/decoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/arithmetic/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/arithmetic/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/baseline.json create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/closure-loop/decoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/closure-loop/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/closure-loop/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/manifest.json create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/scale/decoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/scale/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/readability/scale/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/arithmetic-alias-specialized-212.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/arithmetic.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/cff-anti-2.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/dispatcher-anti-2.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/dispatcher-anti-7.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-alias-specialized-dispatcher-1.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-1.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-2.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-4.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-patch-232.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/generation.json create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/loops-dispatcher-2.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/loops.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/patch-arrays-full-213.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/patch-arrays.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/self-modifying-layers/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-cff/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-closures/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-closures/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-closures/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-methods/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-methods/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-methods/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-nested/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-nested/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-nested/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-switch/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-switch/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/string-concealing-switch/source.js create mode 100644 test/vm/jsconfuser-vm/integration/harness.js create mode 100644 test/vm/jsconfuser-vm/integration/readability-metrics.js create mode 100644 test/vm/jsconfuser-vm/integration/readability.test.js create mode 100644 test/vm/jsconfuser-vm/minified-self-modifying-encoded.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-alias-combinations.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-aliased-wordcode.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-anti-concealed-constants.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-anti-routing-combinations.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-anti-specialized-combinations.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-anti-wordcode.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-cff-wordcode.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-dispatcher-wordcode.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-macro-combinations.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-macro-wordcode.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-self-modifying-wordcode.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-specialized-wordcode.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-string-concealing-combinations.test.js create mode 100644 test/vm/jsconfuser-vm/normalize-string-concealing.test.js create mode 100644 test/vm/jsconfuser-vm/numeric-to-vm-switch.test.js create mode 100644 test/vm/jsconfuser-vm/owned-chain-source.test.js create mode 100644 test/vm/jsconfuser-vm/vm-copy-simplification.test.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/exits/forms/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/exits/forms/source.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/exits/generation.json create mode 100644 test/vm/switch/fixtures/vm-switch-statements/loop-tests/forms/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/loop-tests/forms/source.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/loop-tests/generation.json create mode 100644 test/vm/switch/fixtures/vm-switch-statements/nested-loops/forms/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/nested-loops/forms/source.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/nested-loops/generation.json create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/dowhile-continue/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/dowhile-continue/source.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/forms-cff/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/forms-cff/source.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/forms/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/forms/source.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/generation.json create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break-cff/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break-cff/source.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break/source.js create mode 100644 test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms-cff/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms-cff/source.js create mode 100644 test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms/source.js create mode 100644 test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/generation.json create mode 100644 test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/routes/encoded.js create mode 100644 test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/routes/source.js create mode 100644 test/vm/switch/vm-switch-statements.test.js diff --git a/src/vm/jsconfuser-vm/decode-standalone.js b/src/vm/jsconfuser-vm/decode-standalone.js new file mode 100644 index 00000000..6d8795e9 --- /dev/null +++ b/src/vm/jsconfuser-vm/decode-standalone.js @@ -0,0 +1,121 @@ +import { parse } from '@babel/parser' +import { StandaloneDecline, deepFreeze } from './standalone-contract.js' +import { diagnoseStandaloneInput } from './diagnose-standalone.js' +import { numericToVmSwitch } from './numeric-to-vm-switch.js' +import { VmSwitchDecline } from '../switch/vm-switch-model.js' +import { emitVmSwitchProgram } from '../switch/vm-switch-to-source.js' + +const STANDALONE_SCHEMA = 'jsconfuser-vm-standalone.v2' +const RESIDUE_IDENTIFIERS = new Set([ + 'BYTECODE', + 'CONSTANTS', + 'ENCODE_BYTECODE', + 'VM', + 'Closure', + 'Upvalue', + 'decodeBytecode', +]) + +function validateNumericVmResidue(source) { + const ast = parse(source, { errorRecovery: false, sourceType: 'script' }) + let residue = null + const visit = (node) => { + if (!node || residue) return + if (node.type === 'Identifier' && RESIDUE_IDENTIFIERS.has(node.name)) { + residue = node.name + return + } + if ( + node.type === 'MemberExpression' && + node.object?.type === 'Identifier' && + node.object.name === 'vm' && + node.property?.type === 'Identifier' && + node.property.name === 'run' && + node.computed === false + ) { + residue = 'vm.run' + return + } + for (const value of Object.values(node)) { + if (Array.isArray(value)) { + for (const child of value) if (child?.type) visit(child) + } else if (value?.type) visit(value) + } + } + visit(ast) + if (residue) + throw new VmSwitchDecline( + 'emitted-residue', + `Standalone output contains forbidden VM residue ${residue}`, + ) +} + +export function diagnoseStandalone(source) { + try { + const preflight = diagnoseStandaloneInput(source) + if (!preflight.ok) + return deepFreeze({ + ok: false, + result: null, + diagnostic: preflight.diagnostic, + }) + const model = preflight.model + const switchModel = numericToVmSwitch(model) + const emitted = emitVmSwitchProgram(switchModel) + validateNumericVmResidue(emitted.output) + const result = { + schemaVersion: STANDALONE_SCHEMA, + parseOnly: true, + targetExecution: false, + vmExecuted: false, + emittedJavaScript: true, + finalJavaScriptSemantics: false, + wordCount: model.wordcode.wordCount, + instructionCount: model.wordcode.instructionCount, + functionCount: model.functions.functions.length, + output: emitted.output, + simplification: { + ...emitted.simplification, + schemaVersion: 'jsconfuser-vm-simplification.v1', + }, + coverage: { + instructions: model.wordcode.instructionCount, + functions: model.functions.functions.length, + }, + statements: emitted.statements, + proof: { + allPredecessorsReconstructed: true, + noTargetExecution: true, + noVmRuntimeEmission: true, + freshJavaScriptEmission: true, + freshSimplificationState: true, + simplificationDispositionsDisjoint: true, + finalParseValidated: true, + finalResidueValidated: true, + }, + } + return deepFreeze({ ok: true, result, diagnostic: null }) + } catch (error) { + if (error instanceof StandaloneDecline || error instanceof VmSwitchDecline) + return deepFreeze({ + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }) + return deepFreeze({ + ok: false, + result: null, + diagnostic: { + code: 'standalone-emission-error', + message: error instanceof Error ? error.message : String(error), + }, + }) + } +} + +export function decodeStandalone(source) { + return diagnoseStandalone(source).result?.output ?? null +} + +export const emitStandalone = decodeStandalone +export default decodeStandalone diff --git a/src/vm/jsconfuser-vm/numeric-to-vm-switch.js b/src/vm/jsconfuser-vm/numeric-to-vm-switch.js new file mode 100644 index 00000000..28ac6d9f --- /dev/null +++ b/src/vm/jsconfuser-vm/numeric-to-vm-switch.js @@ -0,0 +1,306 @@ +import { VM_SWITCH_SCHEMA } from '../switch/vm-switch-model.js' + +const BINARY = Object.freeze({ + ADD: '+', + SUB: '-', + MUL: '*', + DIV: '/', + MOD: '%', + EXP: '**', + BAND: '&', + BOR: '|', + BXOR: '^', + SHL: '<<', + SHR: '>>', + USHR: '>>>', + LT: '<', + GT: '>', + LTE: '<=', + GTE: '>=', + EQ: '===', + NEQ: '!==', + LOOSE_EQ: '==', + LOOSE_NEQ: '!=', + IN: 'in', + INSTANCEOF: 'instanceof', +}) +const UNARY = Object.freeze({ + UNARY_NEG: '-', + UNARY_POS: '+', + UNARY_NOT: '!', + UNARY_BITNOT: '~', +}) + +function reg(operand) { + return operand.index +} + +function index(operand) { + return operand.index +} + +function target(operand) { + return operand.pc +} + +function immediate(operand) { + return operand.value +} + +function constant(operand, values) { + return values[operand.index] +} + +function globalName(operand, values) { + return constant(operand, values) +} + +// A plain CALL is the source's `f(...)`: the interpreter's split between its own closures (called +// with its globals) and host functions (applied to null) is its machinery, and the closure shell +// already maps a null `this` to the globals. +function lowerCall(instruction) { + const { name, operands } = instruction + const method = name === 'CALL_METHOD' + const countOffset = method ? 3 : 2 + const countOperand = operands[countOffset] + const spread = countOperand.kind === 'spread-sentinel' + const argumentOperands = operands.slice(countOffset + 1) + return { + kind: 'call', + mode: name === 'NEW' ? 'construct' : 'apply', + destination: reg(operands[0]), + receiver: method + ? { kind: 'register', index: reg(operands[1]) } + : { kind: 'none' }, + callee: reg(operands[method ? 2 : 1]), + arguments: { + kind: spread ? 'array-register' : 'fixed', + registers: argumentOperands.map(reg), + }, + } +} + +function lower(instruction, model) { + const { name, operands: o } = instruction + const values = model.constants + if (name === 'LOAD_CONST') + return { + kind: 'assign', + destination: reg(o[0]), + value: { kind: 'literal', value: constant(o[1], values) }, + } + if (name === 'LOAD_INT') + return { + kind: 'assign', + destination: reg(o[0]), + value: { kind: 'literal', value: immediate(o[1]) }, + } + if (name === 'LOAD_THIS') + return { kind: 'assign', destination: reg(o[0]), value: { kind: 'this' } } + if (name === 'MOVE') + return { + kind: 'assign', + destination: reg(o[0]), + value: { kind: 'register', index: reg(o[1]) }, + } + if (name === 'LOAD_GLOBAL') + return { + kind: 'global-read', + destination: reg(o[0]), + name: globalName(o[1], values), + } + if (name === 'STORE_GLOBAL') + return { + kind: 'global-write', + name: globalName(o[0], values), + source: reg(o[1]), + } + if (name === 'TYPEOF_SAFE') + return { + kind: 'safe-typeof', + destination: reg(o[0]), + name: globalName(o[1], values), + } + if (name === 'LOAD_UPVALUE') + return { + kind: 'upvalue-read', + destination: reg(o[0]), + index: index(o[1]), + } + if (name === 'STORE_UPVALUE') + return { + kind: 'upvalue-write', + index: index(o[0]), + source: reg(o[1]), + } + if (name === 'GET_PROP') + return { + kind: 'property-read', + destination: reg(o[0]), + object: reg(o[1]), + key: reg(o[2]), + } + if (name === 'SET_PROP') + return { + kind: 'property-write', + object: reg(o[0]), + key: reg(o[1]), + value: reg(o[2]), + } + if (name === 'DELETE_PROP') + return { + kind: 'property-delete', + destination: reg(o[0]), + object: reg(o[1]), + key: reg(o[2]), + } + if (BINARY[name]) + return { + kind: 'assign', + destination: reg(o[0]), + value: { + kind: 'binary', + operator: BINARY[name], + left: reg(o[1]), + right: reg(o[2]), + }, + } + if (UNARY[name]) + return { + kind: 'assign', + destination: reg(o[0]), + value: { kind: 'unary', operator: UNARY[name], source: reg(o[1]) }, + } + if (name === 'TYPEOF') + return { + kind: 'assign', + destination: reg(o[0]), + value: { kind: 'typeof', source: reg(o[1]) }, + } + if (name === 'VOID') + return { + kind: 'assign', + destination: reg(o[0]), + value: { kind: 'undefined' }, + } + if (name === 'JUMP') return { kind: 'jump', target: target(o[0]) } + if (name === 'JUMP_IF_FALSE' || name === 'JUMP_IF_TRUE') + return { + kind: 'branch', + condition: reg(o[0]), + when: name === 'JUMP_IF_FALSE' ? 'false' : 'true', + target: target(o[1]), + } + if (name === 'JUMP_REG') return { kind: 'indirect-jump', source: reg(o[0]) } + if (name === 'RETURN') return { kind: 'return', source: reg(o[0]) } + if (name === 'THROW') return { kind: 'throw', source: reg(o[0]) } + if (name === 'CALL' || name === 'CALL_METHOD' || name === 'NEW') + return lowerCall(instruction) + if (name === 'MAKE_CLOSURE') + return { + kind: 'closure', + destination: reg(o[0]), + entry: target(o[1]), + captures: o + .slice(6) + .map((capture) => ({ kind: capture.kind, index: capture.index })), + } + if (name === 'BUILD_ARRAY') + return { + kind: 'array', + destination: reg(o[0]), + elements: o.slice(2).map(reg), + } + if (name === 'BUILD_OBJECT') + return { + kind: 'object', + destination: reg(o[0]), + pairs: Array.from({ length: immediate(o[1]) }, (_, i) => ({ + key: reg(o[2 + i * 2]), + value: reg(o[3 + i * 2]), + })), + } + if (name === 'DEFINE_GETTER' || name === 'DEFINE_SETTER') + return { + kind: 'accessor', + accessor: name === 'DEFINE_GETTER' ? 'get' : 'set', + object: reg(o[0]), + key: reg(o[1]), + callback: reg(o[2]), + } + if (name === 'FOR_IN_SETUP') + return { kind: 'for-in-setup', destination: reg(o[0]), source: reg(o[1]) } + if (name === 'FOR_IN_NEXT') + return { + kind: 'for-in-next', + destination: reg(o[0]), + iterator: reg(o[1]), + exit: target(o[2]), + } + if (name === 'TRY_SETUP') + return { + kind: 'catch-setup', + target: target(o[0]), + exceptionRegister: reg(o[1]), + } + if (name === 'TRY_END') return { kind: 'handler-pop' } + if (name === 'FINALLY_SETUP') + return { + kind: 'finally-setup', + target: target(o[0]), + continuation: reg(o[1]), + payload: reg(o[2]), + throwPad: target(o[3]), + } + if (name === 'DEBUGGER') return { kind: 'debugger' } +} + +export function numericToVmSwitch(model) { + const lowered = new Map() + const instructionsByFunction = new Map() + for (const fn of model.functions.functions) { + const instructions = model.instructionsByFunction.get(fn.id) + const cases = instructions.map((instruction) => { + const result = { + pc: instruction.pc, + nextPc: instruction.nextPc, + operation: lower(instruction, model), + } + lowered.set(instruction.pc, result) + return result + }) + instructionsByFunction.set(fn.id, cases) + } + // The compiler ends a try block with TRY_END; JUMP after_catch right before the catch entry. + // Its target is where the try statement closed in the source; the model carries it as the + // catch-setup's layout end, a placement only. The backend applies it only to a try with no + // reachable end (its block always throws, so that exit is dead code), where the reachable graph + // cannot tell the catch's code from what follows. + for (const cases of instructionsByFunction.values()) { + const byNextPc = new Map(cases.map((item) => [item.nextPc, item])) + for (const { operation } of cases) { + if (operation.kind !== 'catch-setup') continue + const jump = byNextPc.get(operation.target) + const pop = jump && byNextPc.get(jump.pc) + if ( + jump?.operation.kind === 'jump' && + pop?.operation.kind === 'handler-pop' + ) + operation.layoutEnd = jump.operation.target + } + } + return { + schemaVersion: VM_SWITCH_SCHEMA, + storage: { + kind: 'register-machine', + functions: model.functions.functions.map((fn) => ({ + id: fn.id, + registerCount: fn.regCount, + captureCount: fn.captureCount, + })), + }, + functions: model.functions, + instructionsByFunction, + instructionCount: lowered.size, + } +} diff --git a/test/vm/jsconfuser-vm/analyze-anti-runtime.test.js b/test/vm/jsconfuser-vm/analyze-anti-runtime.test.js new file mode 100644 index 00000000..ee6b8f85 --- /dev/null +++ b/test/vm/jsconfuser-vm/analyze-anti-runtime.test.js @@ -0,0 +1,253 @@ +import fs from 'node:fs' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import generate from '@babel/generator' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { expect, test } from 'vitest' +import { diagnoseAntiRuntime } from '../../../src/vm/jsconfuser-vm/analyze-anti-runtime.js' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { + diagnoseAntiWordStream, + diagnoseWordcode, +} from '../../../src/vm/jsconfuser-vm/read-wordcode.js' + +const fixture = fs.readFileSync( + new URL('fixtures/anti-runtime-boot/encoded.js', import.meta.url), + 'utf8', +) +const duplicateStepFixture = fs.readFileSync( + new URL('fixtures/anti-runtime-duplicate-step/encoded.js', import.meta.url), + 'utf8', +) + +function parsed() { + return parse(fixture, { + sourceType: 'script', + allowReturnOutsideFunction: true, + }) +} + +function syntheticCase(ast, opcode) { + let found = null + traverse(ast, { + SwitchCase(path) { + if (path.node.test?.value !== opcode) return + found = path.node + path.stop() + }, + }) + return found +} + +test('observes the connected synthetic operand permutations without read-wordcode admission', () => { + const observation = diagnoseAntiRuntime(fixture) + expect(observation).toMatchObject({ + ok: true, + diagnostic: null, + result: { + schemaVersion: 'jsconfuser-vm-anti-runtime-observation.v1', + syntheticCases: [ + { opcode: 61, arity: 13 }, + { opcode: 62, arity: 15 }, + { opcode: 63, arity: 8 }, + ], + }, + }) + expect(observation.result.syntheticCases[0].canonicalToStored).toEqual([ + 4, 7, 6, 0, 9, 11, 1, 5, 2, 12, 10, 3, 8, + ]) + expect(observation.result.syntheticCases[0].steps).toEqual([ + { name: 'NEQ', operandStart: 0, operandCount: 3 }, + { name: 'ADD', operandStart: 3, operandCount: 3 }, + { name: 'MUL', operandStart: 6, operandCount: 3 }, + { name: 'BUILD_OBJECT', operandStart: 9, operandCount: 4 }, + ]) + expect(Object.isFrozen(observation.result.syntheticCases[0])).toBe(true) + const extracted = diagnoseContainer(fixture) + expect(extracted.ok).toBe(true) + expect(diagnoseWordcode(extracted.result)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'unknown-opcode' }, + }) +}) + +test('segments repeated operator steps without selecting a real step', () => { + const observation = diagnoseAntiRuntime(duplicateStepFixture) + expect(observation.ok).toBe(true) + expect( + observation.result.syntheticCases.find(({ opcode }) => opcode === 62).steps, + ).toEqual([ + { name: 'ADD', operandStart: 0, operandCount: 3 }, + { name: 'JUMP_IF_TRUE', operandStart: 3, operandCount: 2 }, + { name: 'ADD', operandStart: 5, operandCount: 3 }, + ]) +}) + +test('reads synthetic words at exact PCs and restores each step operand span', () => { + for (const source of [fixture, duplicateStepFixture]) { + const observation = diagnoseAntiRuntime(source) + const container = diagnoseContainer(source) + expect(observation.ok).toBe(true) + expect(container.ok).toBe(true) + const read = diagnoseAntiWordStream(container.result, observation.result) + expect(read.ok).toBe(true) + const { instructions, wordCount } = read.result + expect(instructions[0].pc).toBe(0) + expect(instructions.at(-1).nextPc).toBe(wordCount) + expect( + instructions.every( + (instruction, index) => + instruction.nextPc === instruction.pc + instruction.width && + (index === 0 || instructions[index - 1].nextPc === instruction.pc), + ), + ).toBe(true) + const synthetic = instructions.filter(({ name }) => name === 'SYNTHETIC') + expect(synthetic.length).toBeGreaterThan(0) + for (const instruction of synthetic) { + const metadata = observation.result.syntheticCases.find( + ({ opcode }) => opcode === instruction.opcode.value, + ) + expect(instruction.width).toBe(metadata.arity + 1) + expect(instruction.canonicalOperands).toEqual( + metadata.canonicalToStored.map((index) => instruction.words[index + 1]), + ) + expect(instruction.steps.flatMap(({ words }) => words)).toEqual( + instruction.canonicalOperands, + ) + } + expect(Object.isFrozen(synthetic[0].steps[0].words)).toBe(true) + expect(diagnoseWordcode(container.result).diagnostic.code).toBe( + 'unknown-opcode', + ) + } +}) + +test.each(['truncated-instruction', 'unknown-opcode'])( + 'declines source-reachable synthetic word damage at %s', + (code) => { + const runtime = diagnoseAntiRuntime(fixture).result + const container = diagnoseContainer(fixture).result + const stream = diagnoseAntiWordStream(container, runtime).result + const first = stream.instructions.find(({ name }) => name === 'SYNTHETIC') + const original = container.roles.words.values + const words = original.slice() + if (code === 'truncated-instruction') words.length = first.pc + 2 + else words[first.pc] = 65534 + const ast = parsed() + traverse(ast, { + VariableDeclarator(path) { + // The words variable: the array literal holding the container's words. + const values = path.node.init?.elements?.map((node) => node?.value) + if (JSON.stringify(values) !== JSON.stringify(original)) return + path.node.init.elements = words.map((value) => ({ + type: 'NumericLiteral', + value, + })) + path.stop() + }, + }) + const damaged = generate(ast).code + const observed = diagnoseAntiRuntime(damaged) + const admitted = diagnoseContainer(damaged) + expect(observed.ok).toBe(true) + expect(admitted.ok).toBe(true) + expect(admitted.result.roles.words.values).toEqual(words) + expect( + diagnoseAntiWordStream(admitted.result, observed.result), + ).toMatchObject({ + ok: false, + result: null, + diagnostic: { code }, + }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) + }, +) + +test('declines altered synthetic operand reads and permutations atomically', () => { + const duplicatePermutation = parsed() + const case61 = syntheticCase(duplicatePermutation, 61) + case61.consequent[0].body[1].declarations[0].init.elements[1].property.value = + case61.consequent[0].body[1].declarations[0].init.elements[0].property.value + + const changedRead = parsed() + syntheticCase( + changedRead, + 61, + ).consequent[0].body[0].declarations[0].init.elements[0] = { + type: 'NumericLiteral', + value: 0, + } + + const duplicateUse = parsed() + traverse(duplicateUse, { + SwitchCase(path) { + if (path.node.test?.value !== 61) return + path.traverse({ + MemberExpression(inner) { + if ( + inner.node.object?.name !== '_operands' || + inner.node.property?.value !== 1 + ) { + return + } + inner.node.property.value = 0 + inner.stop() + }, + }) + path.stop() + }, + }) + + const missingOpcode = parsed() + syntheticCase(missingOpcode, 62).test.value = 67 + + // Opcodes are named by what their handlers do, never by their values: a moved synthetic label + // leaves the words that use it without a handler, so only the decode declines. + expect(diagnoseStandalone(generate(missingOpcode).code)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) + for (const ast of [duplicatePermutation, changedRead, duplicateUse]) { + expect(diagnoseAntiRuntime(ast)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'anti-runtime' }, + }) + expect(diagnoseStandalone(generate(ast).code)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) + } + + const changedStep = parsed() + traverse(changedStep, { + SwitchCase(path) { + if (path.node.test?.value !== 61) return + path.traverse({ + BinaryExpression(inner) { + if (inner.node.operator !== '!==') return + inner.node.operator = '**' + inner.stop() + }, + }) + path.stop() + }, + }) + expect(diagnoseAntiRuntime(changedStep)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'anti-runtime' }, + }) + expect(diagnoseStandalone(generate(changedStep).code)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) +}) diff --git a/test/vm/jsconfuser-vm/class-runtime-combinations.test.js b/test/vm/jsconfuser-vm/class-runtime-combinations.test.js new file mode 100644 index 00000000..c6c432ce --- /dev/null +++ b/test/vm/jsconfuser-vm/class-runtime-combinations.test.js @@ -0,0 +1,52 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +const dir = new URL( + 'fixtures/class-handler-alias-stack-exceptions/', + import.meta.url, +) +const source = fs.readFileSync(new URL('source.js', dir), 'utf8') +const encoded = fs.readFileSync(new URL('encoded.js', dir), 'utf8') +const record = JSON.parse(fs.readFileSync(new URL('qualification.json', dir))) + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, atob, btoa, performance: { now: () => 0 } }, + { timeout: 30000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test('decodes the eleven-effect class and handler-table stack', () => { + const container = diagnoseContainer(encoded) + expect(container.ok).toBe(true) + expect(container.result.roles.pool.values.some((value) => value < 0)).toBe( + true, + ) + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + const expected = observe(source) + expect(observe(encoded)).toEqual(expected) + expect(observe(decoded.result.output)).toEqual(expected) + expect(record.status).toBe('accepted') + expect(record.effectObservation.observed).toBe(true) + expect( + Object.values(record.effectObservation.components).every(Boolean), + ).toBe(true) +}) diff --git a/test/vm/jsconfuser-vm/control-solver.test.js b/test/vm/jsconfuser-vm/control-solver.test.js new file mode 100644 index 00000000..c601bc0b --- /dev/null +++ b/test/vm/jsconfuser-vm/control-solver.test.js @@ -0,0 +1,136 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseStandaloneInput } from '../../../src/vm/jsconfuser-vm/diagnose-standalone.js' +import { numericToVmSwitch } from '../../../src/vm/jsconfuser-vm/numeric-to-vm-switch.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { solveControl } from '../../../src/vm/switch/vm-switch-control.js' + +const repositoryRoot = new URL('../../../', import.meta.url) + +function sourceAt(relativePath) { + return fs.readFileSync(new URL(relativePath, repositoryRoot), 'utf8') +} + +function solvedAt(relativePath) { + const diagnosis = diagnoseStandaloneInput(sourceAt(relativePath)) + expect(diagnosis.ok, JSON.stringify(diagnosis.diagnostic)).toBe(true) + const { model } = diagnosis + const switchModel = numericToVmSwitch(model) + return model.functions.functions.map((frame) => { + const cases = switchModel.instructionsByFunction.get(frame.id) + return { cases, ...solveControl(frame, cases) } + }) +} + +test('solves finally and abrupt routes with finite indirect targets', () => { + const solved = solvedAt( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-finally-abrupt/encoded.js', + ) + const edges = solved.flatMap(({ edges }) => edges) + const counts = {} + for (const { kind } of edges) counts[kind] = (counts[kind] ?? 0) + 1 + // Each of the 2 catch and 5 finally setups also reaches its active handler; the two protected + // `===` comparisons cannot throw, so they reach none. + expect(counts).toEqual({ + branch: 14, + call: 13, + conditional: 3, + fallthrough: 101, + finally: 22, + handler: 6, + return: 5, + throw: 12, + }) + expect( + solved.flatMap(({ functionId, indirectTargets }) => + indirectTargets.map(({ pc, sourceRegister, targets }) => ({ + functionId, + pc, + sourceRegister, + targets, + })), + ), + ).toEqual([ + { functionId: 1, pc: 170, sourceRegister: 2, targets: [172, 174] }, + { functionId: 2, pc: 245, sourceRegister: 3, targets: [247, 249] }, + { functionId: 3, pc: 303, sourceRegister: 5, targets: [305] }, + { functionId: 3, pc: 362, sourceRegister: 3, targets: [364, 366] }, + { + functionId: 4, + pc: 487, + sourceRegister: 3, + targets: [489, 491, 493, 495], + }, + ]) + for (const { indirectTargets } of solved) + for (const jump of indirectTargets) { + const successors = edges.filter(({ sourcePc }) => sourcePc === jump.pc) + expect(successors).toHaveLength(jump.targets.length) + expect( + successors.every( + ({ kind, mode }) => kind === 'branch' && mode === 'indirect', + ), + ).toBe(true) + } + const protectedEquals = solved.flatMap(({ cases, stateIn }) => + cases.filter( + ({ pc, operation }) => + stateIn.get(pc)?.length && + operation.value?.kind === 'binary' && + operation.value.operator === '===', + ), + ) + expect(protectedEquals).toHaveLength(2) + for (const { pc } of protectedEquals) + expect( + edges.filter(({ sourcePc }) => sourcePc === pc).map(({ kind }) => kind), + ).toEqual(['fallthrough']) +}) + +const deadCode = 'test/vm/jsconfuser-vm/fixtures/dead-code/' + +// The compiler emits source dead code after a return as instructions no path reaches: here a +// try/catch and a try/finally, whose finalizer ends in its resume JUMP_REG. Unreachable code never +// decides behaviour, so the solver reads past them: no edge, no handler state, no indirect target. +test('reads past a try and a finalizer resume no path reaches', () => { + const diagnosis = diagnoseStandaloneInput(sourceAt(deadCode + 'encoded.js')) + expect(diagnosis.ok, JSON.stringify(diagnosis.diagnostic)).toBe(true) + const { model } = diagnosis + const switchModel = numericToVmSwitch(model) + const pcsOf = (...names) => + model.wordcode.instructions + .filter(({ name }) => names.includes(name)) + .map(({ pc }) => pc) + const setups = pcsOf('TRY_SETUP', 'FINALLY_SETUP') + const resumes = pcsOf('JUMP_REG') + expect(setups).toHaveLength(2) + expect(resumes).toHaveLength(1) + const solved = model.functions.functions.map((frame) => + solveControl(frame, switchModel.instructionsByFunction.get(frame.id)), + ) + const reached = new Set(solved.flatMap(({ reachablePcs }) => reachablePcs)) + const touched = new Set( + solved.flatMap(({ edges }) => + edges.flatMap(({ sourcePc, targetPc }) => [sourcePc, targetPc]), + ), + ) + for (const pc of [...setups, ...resumes]) { + expect(reached.has(pc)).toBe(false) + expect(touched.has(pc)).toBe(false) + } + expect(solved.flatMap(({ indirectTargets }) => indirectTargets)).toEqual([]) +}) + +test('decodes dead code after a return with the source behaviour', () => { + const observe = (script) => { + const window = {} + vm.runInNewContext(script, { window }, { timeout: 1000 }) + return window.TEST_OUTPUT + } + const decoded = diagnoseStandalone(sourceAt(deadCode + 'encoded.js')) + expect(decoded.ok, JSON.stringify(decoded.diagnostic)).toBe(true) + expect(observe(decoded.result.output)).toEqual( + observe(sourceAt(deadCode + 'source.js')), + ) +}) diff --git a/test/vm/jsconfuser-vm/decode-standalone.test.js b/test/vm/jsconfuser-vm/decode-standalone.test.js new file mode 100644 index 00000000..7e3877f9 --- /dev/null +++ b/test/vm/jsconfuser-vm/decode-standalone.test.js @@ -0,0 +1,522 @@ +import fs from 'node:fs' +import path from 'node:path' +import vm from 'node:vm' +import { fileURLToPath } from 'node:url' +import { expect, test } from 'vitest' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' +import { + decodeStandalone, + diagnoseStandalone, +} from '../../../src/vm/jsconfuser-vm/decode-standalone.js' + +const repositoryRootUrl = new URL('../../../', import.meta.url) +function corpusCases() { + const entries = [ + { + corpusKind: 'reference', + cellId: 'reference', + relativePath: 'test/vm/jsconfuser-vm/fixtures/reference/encoded.js', + }, + ] + for (const corpusKind of ['raw', 'focused', 'composition']) { + const directory = new URL( + `test/vm/jsconfuser-vm/fixtures/corpus/${corpusKind}/`, + repositoryRootUrl, + ) + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (!entry.isDirectory()) continue + const relativePath = `test/vm/jsconfuser-vm/fixtures/corpus/${corpusKind}/${entry.name}/encoded.js` + if (!fs.existsSync(new URL(relativePath, repositoryRootUrl))) continue + entries.push({ + corpusKind, + cellId: entry.name, + relativePath, + }) + } + } + return entries + .sort((left, right) => left.relativePath.localeCompare(right.relativePath)) + .map((entry) => { + const encodedPath = fileURLToPath( + new URL(entry.relativePath, repositoryRootUrl), + ) + const directory = path.dirname(encodedPath) + return { + ...entry, + encodedSource: fs.readFileSync(encodedPath, 'utf8'), + oracle: JSON.parse( + fs.readFileSync(path.join(directory, 'oracle.json'), 'utf8'), + ), + } + }) +} + +function assertStaticOutput(output) { + const ast = parseSuccessfulOutput(output) + expect(assertNoVmResidue(ast).ok).toBe(true) + expect(assertDerivedStateConsistency(ast, output).ok).toBe(true) +} + +// Known gaps of structured emission (vm-switch-boundary.md item 5); each cell declines there. +const expectedStructuredDeclines = new Set() + +test('recovers the class-obfuscated numeric runtime fixture', () => { + const directory = new URL('fixtures/class-runtime-boot/', import.meta.url) + const source = fs.readFileSync(new URL('source.js', directory), 'utf8') + const encoded = fs.readFileSync(new URL('encoded.js', directory), 'utf8') + const diagnosis = diagnoseStandalone(encoded) + expect(diagnosis.ok).toBe(true) + const decoded = diagnosis.result.output + expect(decoded).toBe(decodeStandalone(encoded)) + assertStaticOutput(decoded) + const observe = (script) => { + const window = {} + vm.runInNewContext( + script, + { window, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return window.TEST_OUTPUT + } + expect(observe(decoded)).toBe(observe(source)) + expect(observe(encoded)).toBe(observe(source)) +}) + +test('recovers every loop nesting as structured loops with source behaviour', () => { + const directory = new URL( + '../switch/fixtures/vm-switch-statements/nested-loops/forms/', + import.meta.url, + ) + const source = fs.readFileSync(new URL('source.js', directory), 'utf8') + const encoded = fs.readFileSync(new URL('encoded.js', directory), 'utf8') + const diagnosis = diagnoseStandalone(encoded) + expect(diagnosis.ok, JSON.stringify(diagnosis.diagnostic)).toBe(true) + const output = diagnosis.result.output + assertStaticOutput(output) + expect(diagnosis.result.statements).toHaveLength( + diagnosis.result.functionCount, + ) + expect(output).not.toContain('__recovered_dispatch') + const observe = (script) => { + const window = {} + vm.runInNewContext(script, { window }, { timeout: 1000 }) + return JSON.stringify(window.TEST_OUTPUT) + } + expect(observe(output)).toBe(observe(source)) +}) + +test('recovers the handler-table numeric runtime fixture', () => { + const directory = new URL('fixtures/handler-table-boot/', import.meta.url) + const source = fs.readFileSync(new URL('source.js', directory), 'utf8') + const encoded = fs.readFileSync(new URL('encoded.js', directory), 'utf8') + const diagnosis = diagnoseStandalone(encoded) + expect(diagnosis.ok).toBe(true) + const decoded = diagnosis.result.output + assertStaticOutput(decoded) + const observe = (script) => { + const window = {} + vm.runInNewContext( + script, + { window, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return window.TEST_OUTPUT + } + expect(observe(decoded)).toBe(observe(source)) + expect(observe(encoded)).toBe(observe(source)) +}) + +function functionOutput(output, functionId) { + const start = output.indexOf(`function __recovered_function_${functionId}(`) + expect(start).toBeGreaterThanOrEqual(0) + const next = output.indexOf('\n function __recovered_function_', start + 1) + return output.slice(start, next === -1 ? output.length : next) +} + +// The backend renders every function structured and publishes its statement regions. +function assertControlContract(diagnosis, output) { + const structured = new Set( + diagnosis.result.statements.map(({ functionId }) => functionId), + ) + expect(structured.size).toBe(diagnosis.result.functionCount) + expect(output).not.toContain('__recovered_pc') + expect(output).not.toContain('__recovered_dispatch') + expect(output).not.toContain('switch (__recovered_pc)') +} + +function isError(value) { + return ( + value && + typeof value === 'object' && + typeof value.name === 'string' && + typeof value.message === 'string' && + (Object.prototype.toString.call(value).endsWith('Error]') || + 'stack' in value) + ) +} + +function project(value, seen = new WeakSet()) { + if (value === undefined) return { type: 'undefined' } + if (typeof value === 'number') { + if (Number.isNaN(value)) return { type: 'nan' } + if (value === Infinity) return { type: 'infinity' } + if (value === -Infinity) return { type: '-infinity' } + if (Object.is(value, -0)) return { type: '-0' } + return value + } + if (typeof value === 'bigint') return { type: 'bigint', value: String(value) } + if (typeof value === 'function') + return { type: 'function', name: value.name || '' } + if (value === null || typeof value !== 'object') return value + if (isError(value)) + return { type: 'error', name: value.name, message: value.message } + if (seen.has(value)) return { type: 'cycle' } + seen.add(value) + const output = Array.isArray(value) + ? value.map((item) => project(item, seen)) + : Object.fromEntries( + Object.keys(value).map((key) => [key, project(value[key], seen)]), + ) + seen.delete(value) + return output +} + +function thrown(value) { + return isError(value) + ? { type: 'error', name: value.name, message: value.message } + : { type: 'primitive', value: project(value) } +} + +function makeExecutionContext(hostProfile) { + const window = { TEST_OUTPUT: null } + if (hostProfile === 'cash-dom-test-v1') { + window.document = { + documentElement: {}, + createElement: function () { + return { style: {} } + }, + } + window.module = { exports: {} } + window.exports = undefined + for (const name of Object.getOwnPropertyNames(globalThis)) { + if (name !== 'globalThis' && !(name in window)) + window[name] = globalThis[name] + } + } + window.window = window + return { context: vm.createContext(window), window } +} + +function observe(window, actions) { + if (!actions?.length) return project(window.TEST_OUTPUT) + const result = {} + const at = (value, keys) => keys.reduce((current, key) => current[key], value) + for (const action of actions) { + const root = action.root === 'window' ? window : window.TEST_OUTPUT + if (action.op === 'keys') + result[action.as] = Object.keys(at(root, action.path || [])) + else if (action.op === 'typeOf') + result[action.as] = typeof at(root, action.path || []) + else if (action.op === 'get') + result[action.as] = project(at(root, action.path || [])) + else if (action.op === 'call') { + const keys = action.path || [] + const parent = keys.length ? at(root, keys.slice(0, -1)) : undefined + const fn = keys.length ? parent[keys[keys.length - 1]] : root + result[action.as] = project(fn.apply(parent, action.args || [])) + } else throw new Error(`Unknown oracle action ${action.op}`) + } + return result +} + +function runRecovered(output, oracle) { + const { context, window } = makeExecutionContext(oracle.hostProfile) + try { + vm.runInContext(output, context, { + timeout: oracle.timeoutMs || 2000, + }) + return { ok: true, value: observe(window, oracle.actions) } + } catch (error) { + return { ok: false, thrown: thrown(error) } + } +} + +function expectedBehavior(oracle) { + return oracle.expectedThrow + ? { ok: false, thrown: oracle.expectedThrow } + : { ok: true, value: oracle.expected } +} + +function replaceBytecodeWords(source, words) { + const match = /var BYTECODE = \[([\s\S]*?)\];/.exec(source) + if (!match) + throw new Error('reference input has no numeric BYTECODE declaration') + const replacement = `var BYTECODE = [${words.join(', ')}];` + return ( + source.slice(0, match.index) + + replacement + + source.slice(match.index + match[0].length) + ) +} + +test('recovers the tracked corpus with complete structure and bounded behavior', () => { + const entries = corpusCases() + expect(entries).toHaveLength(43) + for (const entry of entries) { + const diagnosis = diagnoseStandalone(entry.encodedSource) + if (!diagnosis.ok) { + expect( + expectedStructuredDeclines.has(entry.cellId), + entry.relativePath, + ).toBe(true) + expect(diagnosis.diagnostic.code, entry.relativePath).toBe( + 'unemittable-structured-control', + ) + expect( + decodeStandalone(entry.encodedSource), + entry.relativePath, + ).toBeNull() + continue + } + expect(Object.isFrozen(diagnosis)).toBe(true) + expect(Object.isFrozen(diagnosis.result)).toBe(true) + expect(diagnosis.result.proof).toMatchObject({ + allPredecessorsReconstructed: true, + noTargetExecution: true, + noVmRuntimeEmission: true, + freshJavaScriptEmission: true, + }) + + const output = diagnosis.result.output + assertControlContract(diagnosis, output) + assertStaticOutput(output) + expect(runRecovered(output, entry.oracle), entry.relativePath).toEqual( + expectedBehavior(entry.oracle), + ) + } +}) + +// Each case returns, so each test's consequent is a transfer and the test closes at the next +// one: the switch reads as a chain of guards, `if (value === 1) return "one";` and so on. +test('recovers source-derived switch arms as a chain of guards', () => { + const entry = corpusCases().find( + ({ cellId }) => cellId === 'terminal-switch-arms', + ) + const diagnosis = diagnoseStandalone(entry.encodedSource) + expect(diagnosis.ok).toBe(true) + const conditionals = diagnosis.result.statements + .find(({ functionId }) => functionId === 1) + .regions.filter(({ kind }) => kind === 'conditional') + expect(conditionals).toHaveLength(2) + for (const conditional of conditionals) + expect(conditional.mergeBlockId).toBe(`1:b@${conditional.branchTargetPc}`) + const output = functionOutput(diagnosis.result.output, 1) + expect(output).not.toContain('__recovered_dispatch') + expect(output).not.toContain('else') + expect(runRecovered(diagnosis.result.output, entry.oracle)).toEqual( + expectedBehavior(entry.oracle), + ) +}) + +// Case bodies that share routes (a case falling into the next, grouped cases, a default in the +// middle) have no if/else form: each function's test chain is one switch region, written as a +// native switch whose clauses keep the source order. +test('recovers switches whose case bodies share routes as native switches', () => { + const entry = corpusCases().find(({ cellId }) => cellId === 'f-switch-flow') + const diagnosis = diagnoseStandalone(entry.encodedSource) + expect(diagnosis.ok).toBe(true) + const clauses = { 1: [0, 1, 2, null], 2: [0, 1, null, 2], 3: [0, 1, 2, null] } + for (const [functionId, order] of Object.entries(clauses)) { + const regions = diagnosis.result.statements.find( + (statement) => statement.functionId === Number(functionId), + ).regions + const switches = regions.filter(({ kind }) => kind === 'switch') + expect(switches, `function ${functionId}`).toHaveLength(1) + const [region] = switches + expect(region.clauses.map(({ caseIndex }) => caseIndex)).toEqual(order) + const tests = new Set(region.cases.map(({ testBlockId }) => testBlockId)) + expect( + regions.some( + ({ kind, testBlockId }) => + kind === 'conditional' && tests.has(testBlockId), + ), + ).toBe(false) + const output = functionOutput(diagnosis.result.output, Number(functionId)) + expect(output.match(/switch \(/g)).toHaveLength(1) + expect(output).toContain('default:') + expect(output).not.toContain('__recovered_dispatch') + } + // group's first two cases share one body: the first clause is empty and falls into it. + expect(functionOutput(diagnosis.result.output, 2)).toMatch( + /case [^:]+:\n\s*case /, + ) + expect(runRecovered(diagnosis.result.output, entry.oracle)).toEqual( + expectedBehavior(entry.oracle), + ) +}) + +test('recovers a protected terminal throw and its handler without dispatch', () => { + const entry = corpusCases().find(({ cellId }) => cellId === 'f-throw-catch') + const diagnosis = diagnoseStandalone(entry.encodedSource) + expect(diagnosis.ok).toBe(true) + expect(diagnosis.result.output).toContain('try {') + expect(diagnosis.result.output).toContain('catch (__recovered_error)') + expect(diagnosis.result.output).not.toContain('__recovered_dispatch') + expect(runRecovered(diagnosis.result.output, entry.oracle)).toEqual( + expectedBehavior(entry.oracle), + ) +}) + +test('recovers a protected conditional with caught and normal terminal arms', () => { + const entry = corpusCases().find( + ({ cellId }) => cellId === 'protected-conditional-catch', + ) + const diagnosis = diagnoseStandalone(entry.encodedSource) + expect(diagnosis.ok).toBe(true) + const functionStatements = diagnosis.result.statements.find( + ({ functionId }) => functionId === 1, + ) + // The throwing arm is a transfer, so the merge is the start of the normal arm and the source's + // guard clause `if (value < 0) throw ...; return value + 1;` survives without an else. + const guard = functionStatements.regions.find( + ({ kind }) => kind === 'conditional', + ) + expect(guard).toMatchObject({ + mergeBlockId: `1:b@${guard.branchTargetPc}`, + proof: + 'the arms never meet: the transferring arm ends inside the statement', + }) + expect(functionOutput(diagnosis.result.output, 1)).not.toContain('} else {') + expect(functionOutput(diagnosis.result.output, 1)).toContain('catch (') + expect(functionOutput(diagnosis.result.output, 1)).not.toContain( + '__recovered_dispatch', + ) + expect(runRecovered(diagnosis.result.output, entry.oracle)).toEqual( + expectedBehavior(entry.oracle), + ) +}) + +test('recovers a direct return through a straight finalizer', () => { + const entry = corpusCases().find( + ({ cellId }) => cellId === 'return-through-finally', + ) + const diagnosis = diagnoseStandalone(entry.encodedSource) + expect(diagnosis.ok).toBe(true) + const output = functionOutput(diagnosis.result.output, 1) + expect(output).toContain('try {') + expect(output).toContain('} finally {') + expect(output).not.toContain('__recovered_dispatch') + expect(runRecovered(diagnosis.result.output, entry.oracle)).toEqual( + expectedBehavior(entry.oracle), + ) +}) + +test('recovers caught and normal returns through one finalizer', () => { + const entry = corpusCases().find( + ({ cellId }) => cellId === 'caught-return-through-finally', + ) + const diagnosis = diagnoseStandalone(entry.encodedSource) + expect(diagnosis.ok).toBe(true) + const output = functionOutput(diagnosis.result.output, 1) + expect(output).toContain('catch (__recovered_error)') + expect(output).toContain('} finally {') + expect(output).not.toContain('__recovered_dispatch') + expect(runRecovered(diagnosis.result.output, entry.oracle)).toEqual( + expectedBehavior(entry.oracle), + ) +}) + +test('a damaged finalizer target declines atomically', () => { + const damaged = fs.readFileSync( + new URL( + 'test/vm/jsconfuser-vm/fixtures/corpus/negative/finally-stale-target/encoded.js', + repositoryRootUrl, + ), + 'utf8', + ) + const diagnosis = diagnoseStandalone(damaged) + expect(diagnosis.ok).toBe(false) + expect(diagnosis.result).toBeNull() + expect(decodeStandalone(damaged)).toBeNull() +}) + +test('a damaged control target still declines atomically', () => { + const input = fs.readFileSync( + new URL( + 'test/vm/jsconfuser-vm/fixtures/corpus/negative/control-stale-target/encoded.js', + repositoryRootUrl, + ), + 'utf8', + ) + const diagnosis = diagnoseStandalone(input) + expect(diagnosis).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'unemittable-structured-control' }, + }) + expect(decodeStandalone(input)).toBeNull() +}) + +test('malformed and unsupported containers never emit partial source', () => { + const reference = corpusCases().find( + ({ corpusKind }) => corpusKind === 'reference', + ).encodedSource + const words = [ + 4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 11, 6, 4, 5, 9, 2, 3, + 6, 0, 2, 4, 0, 45, 2, + ] + const mutations = [ + [ + 'truncated wordcode', + replaceBytecodeWords(reference, words.slice(0, -1)), + 'wordcode-declined', + ], + [ + 'extra wordcode', + replaceBytecodeWords(reference, [...words, 0]), + 'wordcode-declined', + ], + [ + 'unknown opcode', + replaceBytecodeWords(reference, [56, ...words.slice(1)]), + 'wordcode-declined', + ], + [ + 'missing bytecode binding', + reference.replace('var BYTECODE =', 'var NOT_BYTECODE ='), + 'container-declined', + ], + [ + 'malformed source', + 'not a valid js-confuser-vm container', + 'container-declined', + ], + ] + for (const [name, input, code] of mutations) { + expect(input, name).not.toBe(reference) + const diagnosis = diagnoseStandalone(input) + expect(diagnosis.ok, name).toBe(false) + expect(diagnosis.result, name).toBeNull() + expect(diagnosis.diagnostic.code, name).toBe(code) + expect(Object.isFrozen(diagnosis), name).toBe(true) + expect(Object.isFrozen(diagnosis.diagnostic), name).toBe(true) + expect(decodeStandalone(input), name).toBeNull() + } +}) + +test('decodeStandalone returns only the diagnosed source', () => { + const input = fs.readFileSync( + new URL( + 'test/vm/jsconfuser-vm/fixtures/reference/encoded.js', + repositoryRootUrl, + ), + 'utf8', + ) + const diagnosis = diagnoseStandalone(input) + expect(diagnosis.ok).toBe(true) + expect(decodeStandalone(input)).toBe(diagnosis.result.output) +}) diff --git a/test/vm/jsconfuser-vm/encoded-bytecode-combinations.test.js b/test/vm/jsconfuser-vm/encoded-bytecode-combinations.test.js new file mode 100644 index 00000000..7966c900 --- /dev/null +++ b/test/vm/jsconfuser-vm/encoded-bytecode-combinations.test.js @@ -0,0 +1,63 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +const fixture = new URL( + 'fixtures/encoded-class-stack-exceptions/', + import.meta.url, +) +const source = fs.readFileSync(new URL('source.js', fixture), 'utf8') +const encoded = fs.readFileSync(new URL('encoded.js', fixture), 'utf8') +const record = JSON.parse( + fs.readFileSync(new URL('qualification.json', fixture)), +) + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, atob, btoa, performance: { now: () => 0 } }, + { timeout: 30000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test('decodes the twelve-effect encoded class stack', () => { + expect(diagnoseContainer(encoded).ok).toBe(true) + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + const expected = observe(source) + expect(observe(encoded)).toEqual(expected) + expect(observe(decoded.result.output)).toEqual(expected) + expect(record.status).toBe('accepted') + expect(record.effectObservation.observed).toBe(true) + expect( + Object.values(record.effectObservation.components).every(Boolean), + ).toBe(true) +}) + +test('declines a damaged class-inlined base64 payload', () => { + const damaged = encoded.replace(/(decodeBytecode\(")[^"]/, '$1*') + expect(damaged).not.toBe(encoded) + expect(diagnoseContainer(damaged)).toMatchObject({ + ok: false, + diagnostic: { code: 'missing-role' }, + }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'container-declined' }, + }) +}) diff --git a/test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/encoded.js b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/encoded.js new file mode 100644 index 00000000..9c44d0d9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/encoded.js @@ -0,0 +1,1135 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = [62, 1, 63, 2, 0, 0, 61, 0, 3, 1, 61, 0, 4, 2, 61, 0, 5, 3, 64, 4, 6, 5, 61, 0, 4, 4, 65, 5, 4, 6, 61, 0, 4, 5, 66, 6, 4, 5, 67, 6, 2, 3, 61, 0, 2, 6, 68, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ALIAS_LOAD_CONST_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 62: + { + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 63: + { + // ALIAS_LOAD_GLOBAL_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 64: + { + // ALIAS_MUL_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a * regs[base + _operands[2]]; + break; + } + case 65: + { + // ALIAS_ADD_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + break; + } + case 66: + { + // ALIAS_MOD_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a % regs[base + _operands[2]]; + break; + } + case 67: + { + // ALIAS_SET_PROP_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + _operands[0]]; + var key = regs[base + _operands[1]]; + var val = regs[base + _operands[2]]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 68: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/qualification.json b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/qualification.json new file mode 100644 index 00000000..28914859 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "arithmetic--aliasedOpcodes--1", + "case": "arithmetic", + "profile": "aliasedOpcodes", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": true, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "wordcode-declined", + "message": "readWordcode declined the input wordcode" + }, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": null, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/source.js b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-boot/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/encoded.js b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/encoded.js new file mode 100644 index 00000000..a4e249b1 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/encoded.js @@ -0,0 +1,1142 @@ +var CONSTANTS = [/* 0 */3, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */undefined, /* 4 */1]; +var BYTECODE = [64, 1, 47, 4, 55, 1, 5, 0, 0, 63, 4, 2, 61, 0, 4, 0, 42, 5, 2, 1, 4, 63, 5, 3, 65, 4, 0, 1, 61, 2, 5, 0, 42, 6, 3, 0, 42, 7, 3, 0, 48, 8, 2, 6, 7, 66, 8, 4, 5, 61, 3, 4, 0, 62, 4, 64, 2, 63, 0, 3, 47, 4, 77, 0, 3, 1, 0, 1, 3, 62, 4, 61, 3, 4, 0, 62, 4, 67, 0, 0, 61, 4, 1, 0, 68, 0, 2, 1, 69, 2, 0, 62, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ALIAS_LOAD_CONST_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 62: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 63: + { + // ALIAS_MOVE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + break; + } + case 64: + { + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 65: + { + // ALIAS_LOAD_GLOBAL_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 66: + { + // ALIAS_SET_PROP_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + _operands[0]]; + var key = regs[base + _operands[1]]; + var val = regs[base + _operands[2]]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 67: + { + // ALIAS_LOAD_UPVALUE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[_operands[1]]._read(); + break; + } + case 68: + { + // ALIAS_ADD_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + break; + } + case 69: + { + // ALIAS_STORE_UPVALUE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var uvIdx = _operands[0]; + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + _operands[1]]); + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/qualification.json b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/qualification.json new file mode 100644 index 00000000..9ef033d6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/qualification.json @@ -0,0 +1,63 @@ +{ + "id": "closures--aliasedOpcodes--2", + "case": "closures", + "profile": "aliasedOpcodes", + "seed": 2, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": true, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "wordcode-declined", + "message": "readWordcode declined the input wordcode" + }, + "sourceObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "decodedObservation": null, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/source.js b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/source.js new file mode 100644 index 00000000..8148a2c5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-closures/source.js @@ -0,0 +1 @@ +function counter(start) { let value = start; return () => ++value; } const next = counter(3); window.TEST_OUTPUT = [next(), next()]; diff --git a/test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/encoded.js new file mode 100644 index 00000000..f35e3146 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/encoded.js @@ -0,0 +1,1218 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */"finished", /* 5 */undefined, /* 6 */0, /* 7 */"Error", /* 8 */"negative", /* 9 */"message", /* 10 */true]; +var BYTECODE = [67, 1, 47, 3, 69, 1, 9, 0, 0, 65, 3, 2, 62, 0, 3, 0, 61, 1, 4, 0, 61, 2, 5, 0, 71, 5, 6, 42, 5, 2, 1, 6, 61, 3, 6, 0, 42, 7, 2, 1, 6, 62, 0, 6, 0, 61, 4, 8, 0, 68, 6, 8, 9, 48, 6, 3, 5, 7, 9, 69, 6, 3, 4, 61, 5, 3, 0, 63, 3, 67, 2, 72, 149, 167, 4, 5, 54, 126, 3, 61, 6, 6, 0, 73, 0, 7, 6, 74, 105, 7, 62, 0, 6, 7, 61, 8, 7, 0, 44, 8, 6, 1, 7, 70, 8, 61, 2, 6, 0, 75, 6, 0, 7, 55, 65, 7, 5, 66, 169, 4, 55, 64, 149, 55, 64, 143, 61, 9, 6, 0, 68, 3, 6, 7, 65, 7, 5, 66, 171, 4, 55, 64, 149, 55, 66, 173, 4, 64, 149, 62, 0, 6, 0, 61, 4, 7, 0, 61, 10, 8, 0, 69, 8, 6, 7, 76, 4, 70, 5, 63, 5, 63, 5, 61, 5, 6, 0, 63, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ALIAS_LOAD_CONST_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 62: + { + // ALIAS_LOAD_GLOBAL_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 63: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 64: + { + // ALIAS_JUMP_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + regs[fp + SLOTS.PC] = _operands[0]; + break; + } + case 65: + { + // ALIAS_MOVE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + break; + } + case 66: + { + // ALIAS_LOAD_INT_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + break; + } + case 67: + { + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 68: + { + // ALIAS_GET_PROP_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + // dst = regs[obj][regs[key]] + var dst = _operands[0]; + var obj = regs[base + _operands[1]]; + var key = regs[base + _operands[2]]; + regs[base + dst] = obj[key]; + break; + } + case 69: + { + // ALIAS_SET_PROP_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + _operands[0]]; + var key = regs[base + _operands[1]]; + var val = regs[base + _operands[2]]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 70: + { + // ALIAS_THROW_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + throw regs[base + _operands[0]]; + break; + } + case 71: + { + // ALIAS_UNARY_NEG_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = -regs[base + _operands[1]]; + break; + } + case 72: + { + // ALIAS_FINALLY_SETUP_0_3_1_2 (order: [0,3,1,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[1]]; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: _operands[0], + contReg: _operands[1], + payloadReg: _operands[2], + throwPad: _operands[3] + }); + break; + } + case 73: + { + // ALIAS_LT_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; + break; + } + case 74: + { + // ALIAS_JUMP_IF_FALSE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var src = _operands[0]; + var target = _operands[1]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 75: + { + // ALIAS_ADD_2_1_0 (order: [2,1,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + break; + } + case 76: + { + // ALIAS_JUMP_REG_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + _operands[0]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/qualification.json new file mode 100644 index 00000000..3d26894f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/qualification.json @@ -0,0 +1,65 @@ +{ + "id": "exceptions--aliasedOpcodes--3", + "case": "exceptions", + "profile": "aliasedOpcodes", + "seed": 3, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": true, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "wordcode-declined", + "message": "readWordcode declined the input wordcode" + }, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": null, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/aliased-runtime-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/encoded.js new file mode 100644 index 00000000..67aa13cc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/encoded.js @@ -0,0 +1,1285 @@ +var CONSTANTS = [/* 0 */"uynkm49", /* 1 */57544, /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */1, /* 5 */"TOVf4x", /* 6 */4, /* 7 */"finished", /* 8 */undefined, /* 9 */41035, /* 10 */54816, /* 11 */"kOYjALm", /* 12 */0, /* 13 */"iYjPR", /* 14 */"Error", /* 15 */"negative", /* 16 */"message", /* 17 */true]; +var BYTECODE = [65, 4, 0, 0, 65, 3, 0, 1, 70, 1, 47, 5, 98, 1, 12, 0, 0, 61, 4, 3, 3, 4, 2, 3, 4, 4, 4, 4, 4, 5, 3, 66, 2, 0, 5, 65, 6, 0, 3, 65, 7, 0, 4, 62, 3, 3, 4, 4, 3, 3, 8, 3, 5, 7, 1, 0, 42, 7, 2, 1, 8, 65, 8, 0, 6, 42, 9, 2, 1, 8, 66, 2, 0, 8, 65, 10, 0, 7, 71, 10, 11, 8, 48, 8, 3, 7, 9, 11, 72, 8, 5, 6, 65, 5, 0, 8, 67, 5, 65, 7, 0, 9, 65, 8, 0, 10, 65, 6, 0, 11, 70, 2, 74, 233, 5, 4, 251, 54, 199, 3, 65, 9, 0, 12, 63, 6, 6, 0, 0, 6, 7, 8, 6, 10, 13, 9, 6, 75, 155, 10, 66, 14, 0, 9, 65, 10, 0, 15, 44, 11, 9, 1, 10, 73, 11, 65, 9, 0, 4, 64, 8, 9, 8, 8, 6, 10, 8, 8, 8, 8, 8, 8, 0, 8, 6, 55, 61, 6, 7, 7, 8, 5, 6, 8, 6, 7, 7, 8, 10, 7, 69, 253, 4, 55, 68, 233, 55, 68, 227, 65, 9, 0, 16, 71, 9, 10, 3, 61, 6, 6, 6, 7, 5, 6, 8, 6, 6, 7, 6, 10, 6, 69, 255, 4, 55, 68, 233, 55, 69, 257, 4, 68, 233, 66, 2, 0, 9, 65, 10, 0, 7, 65, 11, 0, 17, 72, 11, 9, 10, 76, 4, 73, 5, 67, 5, 67, 5, 65, 9, 0, 8, 67, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 65: + { + // ALIAS_LOAD_CONST_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 66: + { + // ALIAS_LOAD_GLOBAL_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 67: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 68: + { + // ALIAS_JUMP_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + regs[fp + SLOTS.PC] = _operands[0]; + break; + } + case 69: + { + // ALIAS_LOAD_INT_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + break; + } + case 70: + { + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 71: + { + // ALIAS_GET_PROP_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + // dst = regs[obj][regs[key]] + var dst = _operands[0]; + var obj = regs[base + _operands[1]]; + var key = regs[base + _operands[2]]; + regs[base + dst] = obj[key]; + break; + } + case 72: + { + // ALIAS_SET_PROP_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + _operands[0]]; + var key = regs[base + _operands[1]]; + var val = regs[base + _operands[2]]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 73: + { + // ALIAS_THROW_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + throw regs[base + _operands[0]]; + break; + } + case 74: + { + // ALIAS_FINALLY_SETUP_0_2_1_3 (order: [0,2,1,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[3]]; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: _operands[0], + contReg: _operands[1], + payloadReg: _operands[2], + throwPad: _operands[3] + }); + break; + } + case 75: + { + // ALIAS_JUMP_IF_FALSE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var src = _operands[0]; + var target = _operands[1]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 76: + { + // ALIAS_JUMP_REG_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + _operands[0]]; + break; + } + case 61: + { + // ANTI_MOVE_0_10_3_6_11_8_1_4_2_7_5_12_9 (order: [0,10,3,6,11,8,1,4,2,7,5,12,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[12], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[11]]; + // GTE + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >= regs[base + _operands[2]]; + // USHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >>> regs[base + _operands[5]]; + // GTE + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >= regs[base + _operands[8]]; + // TYPEOF + var dst = _operands[9]; + regs[base + dst] = typeof regs[base + _operands[10]]; + // MOVE + var dst = _operands[11]; + regs[base + dst] = regs[base + _operands[12]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_8_2_4_3_9_11_0_7_5_1_10_6 (order: [8,2,4,3,9,11,0,7,5,1,10,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[11], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[10], _unsortedOperands[5]]; + // UNARY_NEG + var dst = _operands[0]; + regs[base + dst] = -regs[base + _operands[1]]; + // UNARY_POS + var dst = _operands[2]; + regs[base + dst] = +regs[base + _operands[3]]; + // LOAD_CONST + var dst = _operands[4]; + regs[base + dst] = this._constant(_operands[5], _operands[6]); + // MOVE + var dst = _operands[7]; + regs[base + dst] = regs[base + _operands[8]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[9]; + var count = _operands[10]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[11]]; + regs[base + dst] = elems; + break; + } + case 63: + { + // ANTI_LT_5_4_7_11_1_9_2_0_6_10_8_3 (order: [5,4,7,11,1,9,2,0,6,10,8,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[11], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[3]]; + // LOOSE_NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a != regs[base + _operands[2]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[5]]); + // LT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a < regs[base + _operands[8]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[9]; + var name = this._constant(_operands[10], _operands[11]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + case 64: + { + // ANTI_ADD_9_8_12_10_13_6_5_11_1_2_0_14_7_4_3 (order: [9,8,12,10,13,6,5,11,1,2,0,14,7,4,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[14], _unsortedOperands[13], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[12], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[11]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // EQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a === regs[base + _operands[5]]; + // ADD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a + regs[base + _operands[8]]; + // MUL + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a * regs[base + _operands[11]]; + // SUB + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a - regs[base + _operands[14]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/qualification.json new file mode 100644 index 00000000..70cb6984 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/qualification.json @@ -0,0 +1,73 @@ +{ + "id": "exceptions--antiInstrumentationAliasedOpcodes--113", + "case": "exceptions", + "profile": "antiInstrumentationAliasedOpcodes", + "seed": 113, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": true, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "antiInstrumentation": true, + "aliasedOpcodes": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-alias-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/encoded.js new file mode 100644 index 00000000..f3668226 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/encoded.js @@ -0,0 +1,1279 @@ +var CONSTANTS = [/* 0 */"RKTvWn0", /* 1 */"zook", /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */1, /* 5 */4, /* 6 */"finished", /* 7 */undefined, /* 8 */16120, /* 9 */54803, /* 10 */55500, /* 11 */"idZip3", /* 12 */0, /* 13 */"Error", /* 14 */"negative", /* 15 */"message", /* 16 */true]; +var BYTECODE = [69, 3, 0, 0, 4, 1, 0, 1, 47, 5, 92, 1, 14, 0, 0, 61, 5, 3, 4, 4, 2, 3, 3, 3, 3, 3, 4, 70, 5, 2, 0, 6, 3, 0, 7, 4, 0, 62, 3, 4, 3, 4, 4, 4, 4, 3, 4, 4, 4, 7, 3, 8, 42, 7, 2, 1, 8, 0, 8, 5, 0, 42, 9, 2, 1, 8, 66, 8, 2, 0, 10, 6, 0, 8, 11, 8, 10, 48, 8, 3, 7, 9, 11, 9, 5, 6, 8, 73, 5, 7, 0, 5, 68, 7, 8, 0, 9, 9, 0, 6, 10, 0, 8, 11, 0, 71, 10, 12, 0, 2, 59, 225, 4, 5, 241, 54, 194, 3, 0, 11, 12, 0, 63, 7, 11, 7, 7, 12, 6, 7, 6, 10, 6, 6, 0, 137, 6, 40, 12, 154, 66, 11, 13, 0, 12, 14, 0, 44, 13, 11, 1, 12, 46, 13, 0, 11, 4, 0, 64, 12, 8, 6, 8, 6, 8, 6, 11, 8, 17992, 8, 8, 0, 55, 61, 12, 6, 6, 8, 5, 8, 8, 6, 8, 8, 8, 1, 4, 243, 55, 39, 225, 55, 39, 219, 72, 11, 15, 0, 12, 3, 11, 61, 12, 8, 8, 6, 5, 8, 8, 8, 8, 8, 8, 1, 4, 245, 55, 39, 225, 55, 1, 4, 247, 39, 225, 70, 11, 2, 0, 12, 6, 0, 13, 16, 0, 9, 11, 12, 13, 58, 4, 46, 5, 45, 5, 45, 5, 73, 11, 7, 0, 11]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 65: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 66: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 67: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 68: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 69: + { + // LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 70: + { + // LOAD_GLOBAL,LOAD_CONST,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 71: + { + // LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 72: + { + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 73: + { + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 61: + { + // ANTI_MOVE_10_3_2_7_9_1_8_5_0_4_6 (order: [10,3,2,7,9,1,8,5,0,4,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[0]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[2]]); + // USHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >>> regs[base + _operands[5]]; + // ADD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a + regs[base + _operands[8]]; + // MOVE + var dst = _operands[9]; + regs[base + dst] = regs[base + _operands[10]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_8_3_6_5_12_4_2_13_1_0_11_10_7_9 (order: [8,3,6,5,12,4,2,13,1,0,11,10,7,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[12], _unsortedOperands[0], _unsortedOperands[13], _unsortedOperands[11], _unsortedOperands[10], _unsortedOperands[4], _unsortedOperands[7]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[5]]); + // GT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a > regs[base + _operands[8]]; + // UNARY_NEG + var dst = _operands[9]; + regs[base + dst] = -regs[base + _operands[10]]; + // NEQ + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a !== regs[base + _operands[13]]; + break; + } + case 63: + { + // ANTI_LT_12_5_13_6_3_1_7_11_9_2_8_4_10_0 (order: [12,5,13,6,3,1,7,11,9,2,8,4,10,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[13], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[12], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[2]]; + // LOOSE_EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a == regs[base + _operands[2]]; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // DIV + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a / regs[base + _operands[8]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[9]; + var target = _operands[10]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // MUL + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a * regs[base + _operands[13]]; + break; + } + case 64: + { + // ANTI_ADD_0_10_6_8_5_7_9_2_11_12_3_4_1 (order: [0,10,6,8,5,7,9,2,11,12,3,4,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[12], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[9]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // TYPEOF + var dst = _operands[3]; + regs[base + dst] = typeof regs[base + _operands[4]]; + // LT + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a < regs[base + _operands[7]]; + // EQ + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a === regs[base + _operands[10]]; + // LOAD_INT + var dst = _operands[11]; + regs[base + dst] = _operands[12]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/qualification.json new file mode 100644 index 00000000..cf4875b0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/qualification.json @@ -0,0 +1,73 @@ +{ + "id": "exceptions--antiInstrumentationMacroOpcodes--98", + "case": "exceptions", + "profile": "antiInstrumentationMacroOpcodes", + "seed": 98, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": true, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "antiInstrumentation": true, + "macroOpcodes": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-macro-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/encoded.js b/test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/encoded.js new file mode 100644 index 00000000..76071b70 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/encoded.js @@ -0,0 +1,1134 @@ +var CONSTANTS = [/* 0 */"GeY0qk", /* 1 */21254, /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */1, /* 5 */4, /* 6 */"finished", /* 7 */undefined, /* 8 */9419, /* 9 */0, /* 10 */"FGn", /* 11 */51917, /* 12 */"uhiesP", /* 13 */"Error", /* 14 */"negative", /* 15 */"Number", /* 16 */"message", /* 17 */true]; +var BYTECODE = [0, 4, 0, 0, 0, 3, 1, 0, 4, 1, 47, 5, 95, 1, 14, 0, 0, 61, 4, 5, 4, 2, 3, 3, 3, 4, 3, 2, 3, 4, 2, 5, 2, 0, 0, 6, 3, 0, 0, 7, 4, 0, 62, 7, 3, 8, 4, 4, 4, 4, 4, 4, 13409, 42, 7, 2, 1, 8, 0, 8, 5, 0, 42, 9, 2, 1, 8, 2, 8, 2, 0, 0, 10, 6, 0, 8, 11, 8, 10, 48, 8, 3, 7, 9, 11, 9, 5, 6, 8, 0, 5, 7, 0, 45, 5, 0, 6, 8, 0, 0, 10, 9, 0, 0, 8, 10, 0, 0, 9, 11, 0, 0, 7, 12, 0, 4, 2, 59, 236, 4, 5, 254, 54, 203, 3, 0, 11, 9, 0, 63, 6, 8, 12, 7, 6, 6, 6, 1, 11, 0, 6, 6, 8, 7, 6, 40, 12, 163, 2, 11, 13, 0, 0, 12, 14, 0, 44, 13, 11, 1, 12, 46, 13, 0, 11, 4, 0, 64, 10, 6, 180, 15, 7, 11, 0, 8, 180, 12, 10, 0, 55, 61, 7, 12, 7, 5, 7, 6, 7, 8, 7, 2, 6, 6, 1, 4, 256, 55, 39, 236, 55, 39, 230, 0, 11, 16, 0, 8, 12, 3, 11, 61, 6, 12, 7, 5, 6, 6, 6, 7, 6, 0, 7, 6, 1, 4, 258, 55, 39, 236, 55, 1, 4, 260, 39, 236, 2, 11, 2, 0, 0, 12, 6, 0, 0, 13, 17, 0, 9, 11, 12, 13, 58, 4, 46, 5, 45, 5, 45, 5, 0, 11, 7, 0, 45, 11]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_5_7_11_6_1_2_0_3_10_9_4_8 (order: [5,7,11,6,1,2,0,3,10,9,4,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[11], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[2]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // MOVE + var dst = _operands[6]; + regs[base + dst] = regs[base + _operands[7]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[8]; + var pairCount = _operands[9]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[10]]; + var val = regs[base + _operands[11]]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + case 62: + { + // ANTI_UNARY_NEG_9_6_8_0_5_3_4_1_2_7 (order: [9,6,8,0,5,3,4,1,2,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[0]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // LOAD_INT + var dst = _operands[6]; + regs[base + dst] = _operands[7]; + // UNARY_NEG + var dst = _operands[8]; + regs[base + dst] = -regs[base + _operands[9]]; + break; + } + case 63: + { + // ANTI_LT_14_11_5_13_2_4_9_12_7_6_0_3_1_8_10 (order: [14,11,5,13,2,4,9,12,7,6,0,3,1,8,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[12], _unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[13], _unsortedOperands[6], _unsortedOperands[14], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[0]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // BXOR + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a ^ regs[base + _operands[4]]; + // LT + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a < regs[base + _operands[7]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[10]]); + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[11]; + var pairCount = _operands[12]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[13]]; + var val = regs[base + _operands[14]]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + case 64: + { + // ANTI_ADD_7_2_8_3_0_11_4_1_6_9_5_10 (order: [7,2,8,3,0,11,4,1,6,9,5,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[11], _unsortedOperands[5]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // LOAD_GLOBAL + var dst = _operands[2]; + var globalName = this._constant(_operands[3], _operands[4]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[5]; + var target = _operands[6]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[7]; + var target = _operands[8]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // ADD + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a + regs[base + _operands[11]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/qualification.json b/test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/qualification.json new file mode 100644 index 00000000..5df5c58d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/qualification.json @@ -0,0 +1,69 @@ +{ + "id": "exceptions--antiInstrumentation--1", + "case": "exceptions", + "profile": "antiInstrumentation", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/source.js b/test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-catch-finally/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/encoded.js b/test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/encoded.js new file mode 100644 index 00000000..d98a4503 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/encoded.js @@ -0,0 +1,1070 @@ +var CONSTANTS = [/* 0 */16332, /* 1 */21189, /* 2 */49610, /* 3 */0, /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */4, /* 7 */undefined, /* 8 */17344, /* 9 */40777, /* 10 */"9Sys", /* 11 */1]; +var BYTECODE = [0, 6, 0, 0, 0, 5, 1, 0, 0, 4, 2, 0, 4, 1, 47, 7, 85, 1, 11, 1, 0, 1, 2, 61, 4, 7, 5, 3, 6, 6, 6, 4, 4, 2, 4, 5, 0, 7, 3, 0, 61, 5, 7, 4, 2, 4, 6, 4, 5, 4, 2, 4, 4, 2, 7, 4, 0, 0, 8, 5, 0, 0, 9, 6, 0, 42, 10, 3, 1, 9, 48, 9, 2, 10, 2, 9, 7, 8, 9, 0, 7, 7, 0, 45, 7, 0, 5, 8, 0, 0, 6, 9, 0, 0, 7, 10, 0, 4, 2, 59, 145, 3, 4, 182, 0, 8, 11, 0, 62, 42692, 7, 8, 0, 7, 7, 5, 7, 7, 7, 9, 61, 5, 9, 7, 4, 7, 5, 5, 5, 5, 0, 7, 5, 1, 3, 184, 55, 39, 145, 55, 1, 3, 186, 39, 145, 3, 8, 0, 61, 5, 8, 6, 9, 5, 7, 5, 5, 5, 2, 5, 5, 0, 9, 11, 0, 62, 2398, 5, 9, 8, 7, 7, 5, 5, 5, 5, 10, 7, 0, 10, 58, 3, 46, 4, 45, 4, 0, 8, 7, 0, 45, 8]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_5_7_11_6_1_2_0_3_10_9_4_8 (order: [5,7,11,6,1,2,0,3,10,9,4,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[11], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[2]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // MOVE + var dst = _operands[6]; + regs[base + dst] = regs[base + _operands[7]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[8]; + var pairCount = _operands[9]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[10]]; + var val = regs[base + _operands[11]]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + case 62: + { + // ANTI_ADD_7_6_10_9_0_4_3_5_1_2_8 (order: [7,6,10,9,0,4,3,5,1,2,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[2]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // LOAD_INT + var dst = _operands[6]; + regs[base + dst] = _operands[7]; + // ADD + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a + regs[base + _operands[10]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/qualification.json b/test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/qualification.json new file mode 100644 index 00000000..10f390d4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "tryFinally--antiInstrumentation--1", + "case": "tryFinally", + "profile": "antiInstrumentation", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/source.js b/test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/source.js new file mode 100644 index 00000000..747d80bf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-runtime-finally/source.js @@ -0,0 +1 @@ +let count = 0; function check(value) { try { return value + 1; } finally { count++; } } window.TEST_OUTPUT = [check(4), count]; diff --git a/test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/encoded.js new file mode 100644 index 00000000..cdb5b187 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/encoded.js @@ -0,0 +1,1465 @@ +var CONSTANTS = [/* 0 */"EZozu", /* 1 */15650, /* 2 */33813, /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */1, /* 6 */"351", /* 7 */4, /* 8 */"finished", /* 9 */undefined, /* 10 */14394, /* 11 */62011, /* 12 */49231, /* 13 */0, /* 14 */"Error", /* 15 */"negative", /* 16 */"message", /* 17 */true]; +var BYTECODE = [67, 68, 69, 70, 47, 6, 77, 1, 12, 0, 0, 61, 3, 4, 4, 1, 5, 4, 2, 4, 4, 3, 6, 4, 71, 72, 0, 8, 5, 0, 62, 3, 5, 8, 4, 9, 4, 3, 4, 5, 3, 4, 5, 6, 0, 42, 8, 2, 1, 9, 0, 9, 7, 0, 42, 10, 2, 1, 9, 73, 0, 11, 8, 0, 74, 48, 9, 3, 8, 10, 12, 75, 0, 6, 9, 0, 76, 0, 6, 10, 0, 0, 7, 11, 0, 0, 8, 12, 0, 77, 78, 79, 0, 9, 13, 0, 63, 6, 6, 6, 2, 10, 6, 0, 9, 6, 6, 6, 6, 6, 6, 80, 81, 0, 10, 15, 0, 44, 11, 9, 1, 10, 82, 0, 9, 5, 0, 64, 7, 8, 7, 6, 0, 8, 7, 6, 7, 10, 8, 8, 9, 55, 61, 6, 7, 7, 1, 6, 8, 5, 7, 7, 7, 10, 7, 83, 55, 65, 55, 84, 0, 9, 16, 0, 85, 61, 8, 6, 6, 2, 7, 8, 5, 7, 6, 8, 10, 8, 86, 55, 65, 55, 87, 65, 88, 0, 10, 8, 0, 0, 11, 17, 0, 89, 90, 91, 66, 66, 0, 9, 9, 0, 92]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 13; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 65: + { + // JUMP_184 (specialized) + regs[fp + SLOTS.PC] = 184; + break; + } + case 66: + { + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 67: + { + // LOAD_CONST_3_0_0 (specialized) + var dst = 3; + regs[base + dst] = this._constant(0, 0); + break; + } + case 68: + { + // LOAD_CONST_5_1_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(1, 0); + break; + } + case 69: + { + // LOAD_CONST_4_2_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(2, 0); + break; + } + case 70: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 71: + { + // LOAD_GLOBAL_6_3_0 (specialized) + var dst = 6; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 72: + { + // LOAD_CONST_7_4_0 (specialized) + var dst = 7; + regs[base + dst] = this._constant(4, 0); + break; + } + case 73: + { + // LOAD_GLOBAL_9_3_0 (specialized) + var dst = 9; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 74: + { + // GET_PROP_12_9_11 (specialized) + // dst = regs[obj][regs[key]] + var dst = 12; + var obj = regs[base + 9]; + var key = regs[base + 11]; + regs[base + dst] = obj[key]; + break; + } + case 75: + { + // SET_PROP_6_7_9 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 6]; + var key = regs[base + 7]; + var val = regs[base + 9]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 76: + { + // RETURN_6 (specialized) + var retVal = regs[base + 6]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 77: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 78: + { + // FINALLY_SETUP_184_4_5_195 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 184, + contReg: 4, + payloadReg: 5, + throwPad: 195 + }); + break; + } + case 79: + { + // TRY_SETUP_160_3 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 160, + exceptionReg: 3 + }); + break; + } + case 80: + { + // JUMP_IF_FALSE_10_123 (specialized) + var src = 10; + var target = 123; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 81: + { + // LOAD_GLOBAL_9_14_0 (specialized) + var dst = 9; + var globalName = this._constant(14, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 82: + { + // THROW_11 (specialized) + throw regs[base + 11]; + break; + } + case 83: + { + // LOAD_INT_4_196 (specialized) + var dst = 4; + regs[base + dst] = 196; + break; + } + case 84: + { + // JUMP_181 (specialized) + regs[fp + SLOTS.PC] = 181; + break; + } + case 85: + { + // GET_PROP_10_3_9 (specialized) + // dst = regs[obj][regs[key]] + var dst = 10; + var obj = regs[base + 3]; + var key = regs[base + 9]; + regs[base + dst] = obj[key]; + break; + } + case 86: + { + // LOAD_INT_4_197 (specialized) + var dst = 4; + regs[base + dst] = 197; + break; + } + case 87: + { + // LOAD_INT_4_198 (specialized) + var dst = 4; + regs[base + dst] = 198; + break; + } + case 88: + { + // LOAD_GLOBAL_9_3_0 (specialized) + var dst = 9; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 89: + { + // SET_PROP_9_10_11 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 9]; + var key = regs[base + 10]; + var val = regs[base + 11]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 90: + { + // JUMP_REG_4 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 4]; + break; + } + case 91: + { + // THROW_5 (specialized) + throw regs[base + 5]; + break; + } + case 92: + { + // RETURN_9 (specialized) + var retVal = regs[base + 9]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 61: + { + // ANTI_MOVE_7_10_5_6_9_0_3_11_8_2_4_1 (order: [7,10,5,6,9,0,3,11,8,2,4,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[11], _unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[7]]; + // LTE + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a <= regs[base + _operands[2]]; + // MOVE + var dst = _operands[3]; + regs[base + dst] = regs[base + _operands[4]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[5]; + var pairCount = _operands[6]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[7]]; + var val = regs[base + _operands[8]]; + o[key] = val; + } + regs[base + dst] = o; + // ADD + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a + regs[base + _operands[11]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_2_10_4_0_3_9_11_7_5_6_1_8_12_13 (order: [2,10,4,0,3,9,11,7,5,6,1,8,12,13]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[12], _unsortedOperands[13]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // UNARY_NEG + var dst = _operands[3]; + regs[base + dst] = -regs[base + _operands[4]]; + // ADD + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a + regs[base + _operands[7]]; + // GTE + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a >= regs[base + _operands[10]]; + // LOAD_CONST + var dst = _operands[11]; + regs[base + dst] = this._constant(_operands[12], _operands[13]); + break; + } + case 63: + { + // ANTI_LT_6_4_3_7_0_8_1_2_12_11_5_9_10_13 (order: [6,4,3,7,0,8,1,2,12,11,5,9,10,13]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[13]]; + // LT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; + // DIV + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a / regs[base + _operands[5]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[6]; + var count = _operands[7]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[8]]; + regs[base + dst] = elems; + // LTE + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a <= regs[base + _operands[11]]; + // UNARY_POS + var dst = _operands[12]; + regs[base + dst] = +regs[base + _operands[13]]; + break; + } + case 64: + { + // ANTI_ADD_10_0_1_9_4_2_7_11_6_3_8_12_5 (order: [10,0,1,9,4,2,7,11,6,3,8,12,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[12], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[11]]; + // DIV + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a / regs[base + _operands[2]]; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // MOVE + var dst = _operands[6]; + regs[base + dst] = regs[base + _operands[7]]; + // ADD + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a + regs[base + _operands[10]]; + // MOVE + var dst = _operands[11]; + regs[base + dst] = regs[base + _operands[12]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/qualification.json new file mode 100644 index 00000000..436ba349 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/qualification.json @@ -0,0 +1,73 @@ +{ + "id": "exceptions--antiInstrumentationSpecializedOpcodes--91", + "case": "exceptions", + "profile": "antiInstrumentationSpecializedOpcodes", + "seed": 91, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "antiInstrumentation": true, + "specializedOpcodes": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/anti-specialized-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/encoded.js new file mode 100644 index 00000000..b341b87a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/encoded.js @@ -0,0 +1,1179 @@ +var CONSTANTS = [/* 0 */26094, /* 1 */"9Rn", /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */1, /* 5 */4, /* 6 */"finished", /* 7 */undefined, /* 8 */28573, /* 9 */"lqynJ0x", /* 10 */"7sIRtW", /* 11 */54904, /* 12 */0, /* 13 */40111, /* 14 */15689, /* 15 */53209, /* 16 */8672, /* 17 */"oVYX", /* 18 */"AgR", /* 19 */32125, /* 20 */"oey5AB", /* 21 */"mQzfOwAp", /* 22 */6603, /* 23 */51279, /* 24 */54654, /* 25 */1544, /* 26 */"FJ78w6lK", /* 27 */25160, /* 28 */"SKlDqF8Z", /* 29 */19699, /* 30 */48164, /* 31 */38218, /* 32 */"FIZe12", /* 33 */"z1vI0m8", /* 34 */51177, /* 35 */"MboFfIYk", /* 36 */3286, /* 37 */"Ymh", /* 38 */12353, /* 39 */23893, /* 40 */"pUL2JH", /* 41 */31092, /* 42 */60959, /* 43 */885, /* 44 */41040, /* 45 */"VlIdc", /* 46 */17647, /* 47 */"GdWyx", /* 48 */"lnuHvc", /* 49 */8980, /* 50 */56423, /* 51 */26019, /* 52 */10583, /* 53 */65191, /* 54 */"Error", /* 55 */"negative", /* 56 */950, /* 57 */63093, /* 58 */49845, /* 59 */47570, /* 60 */11977, /* 61 */23637, /* 62 */"5lrYSjPm", /* 63 */49166, /* 64 */"jON1dxe", /* 65 */"message", /* 66 */"7bOWQ", /* 67 */true]; +var BYTECODE = [0, 4, 0, 0, 0, 3, 1, 0, 4, 1, 47, 5, 95, 1, 71, 0, 0, 61, 4, 5, 2, 3, 3, 3, 3, 3, 3, 4, 3, 3, 4, 3, 2, 5, 2, 0, 0, 6, 3, 0, 0, 7, 4, 0, 62, 0, 4, 8, 7, 4, 4, 3, 3, 42, 7, 2, 1, 8, 0, 8, 5, 0, 42, 9, 2, 1, 8, 2, 8, 2, 0, 0, 10, 6, 0, 8, 11, 8, 10, 48, 8, 3, 7, 9, 11, 9, 5, 6, 8, 0, 5, 7, 0, 45, 5, 0, 67, 8, 0, 0, 66, 9, 0, 0, 65, 10, 0, 4, 10, 0, 11, 11, 0, 61, 67, 11, 12, 66, 66, 66, 65, 66, 65, 65, 66, 66, 67, 66, 0, 13, 12, 0, 61, 66, 13, 14, 66, 65, 67, 66, 65, 66, 67, 65, 66, 66, 67, 0, 15, 13, 0, 63, 15, 65, 65, 65, 16, 65, 65, 65, 12, 40, 16, 919, 0, 17, 14, 0, 61, 67, 17, 14, 65, 67, 67, 66, 65, 65, 65, 65, 65, 65, 65, 64, 12, 67, 0, 65, 66, 14, 18, 15, 67, 40, 18, 198, 39, 993, 0, 19, 16, 0, 65, 19, 67, 67, 67, 14, 0, 17, 67, 65, 20, 65, 65, 61, 65, 20, 14, 67, 67, 65, 67, 67, 65, 65, 65, 67, 65, 65, 64, 12, 66, 0, 66, 65, 14, 21, 18, 66, 40, 21, 245, 39, 939, 0, 22, 19, 0, 65, 22, 66, 67, 66, 14, 0, 20, 66, 66, 23, 66, 66, 61, 65, 23, 14, 67, 65, 66, 66, 65, 65, 65, 67, 65, 66, 67, 64, 12, 67, 0, 66, 67, 14, 24, 21, 67, 40, 24, 292, 39, 934, 0, 25, 22, 0, 65, 25, 65, 65, 65, 14, 0, 23, 65, 66, 26, 66, 66, 61, 66, 26, 14, 67, 66, 65, 65, 65, 65, 65, 67, 66, 65, 65, 64, 12, 67, 0, 67, 66, 14, 27, 24, 67, 40, 27, 339, 39, 1271, 0, 28, 25, 0, 66, 65, 65, 29, 65, 65, 65, 28, 65, 65, 14, 65, 65, 65, 65, 61, 67, 29, 14, 65, 66, 66, 65, 65, 67, 67, 65, 65, 66, 65, 64, 12, 66, 0, 65, 65, 14, 30, 26, 65, 40, 30, 388, 39, 1205, 0, 31, 27, 0, 66, 65, 67, 32, 65, 65, 65, 31, 65, 65, 14, 67, 65, 65, 67, 61, 67, 32, 14, 66, 67, 65, 67, 65, 66, 65, 65, 65, 66, 65, 64, 12, 66, 0, 67, 66, 14, 33, 28, 67, 40, 33, 437, 39, 1028, 0, 34, 29, 0, 66, 65, 67, 35, 66, 65, 65, 34, 67, 67, 14, 65, 65, 66, 66, 61, 67, 35, 14, 65, 66, 65, 65, 65, 66, 65, 65, 67, 65, 65, 64, 12, 65, 0, 65, 66, 14, 36, 30, 67, 40, 36, 486, 39, 1200, 0, 37, 31, 0, 65, 37, 65, 65, 65, 14, 0, 32, 66, 65, 38, 67, 66, 61, 66, 38, 14, 66, 65, 67, 66, 66, 67, 67, 66, 67, 66, 66, 64, 12, 65, 0, 67, 65, 14, 39, 33, 65, 40, 39, 533, 39, 998, 0, 40, 34, 0, 66, 65, 66, 41, 67, 65, 66, 40, 65, 65, 14, 65, 65, 65, 65, 61, 66, 41, 14, 67, 67, 65, 67, 66, 66, 66, 65, 65, 67, 66, 64, 12, 65, 0, 67, 66, 14, 42, 35, 67, 40, 42, 582, 39, 975, 0, 43, 36, 0, 66, 65, 67, 44, 65, 65, 65, 43, 67, 67, 14, 65, 65, 66, 65, 61, 66, 44, 14, 66, 66, 66, 65, 66, 65, 65, 67, 67, 66, 66, 64, 12, 66, 0, 66, 65, 14, 45, 37, 65, 40, 45, 631, 39, 1227, 0, 46, 38, 0, 65, 46, 67, 67, 67, 14, 0, 39, 67, 67, 47, 66, 67, 61, 65, 47, 14, 65, 66, 66, 67, 67, 66, 65, 67, 67, 65, 67, 64, 12, 66, 0, 65, 65, 14, 48, 40, 65, 40, 48, 678, 39, 1086, 0, 49, 41, 0, 65, 49, 67, 65, 67, 14, 0, 42, 65, 67, 50, 67, 67, 61, 67, 50, 14, 66, 66, 65, 65, 65, 65, 65, 65, 67, 65, 66, 64, 12, 65, 0, 66, 65, 14, 51, 43, 66, 40, 51, 725, 39, 1176, 0, 52, 44, 0, 66, 67, 67, 53, 67, 67, 67, 52, 67, 67, 14, 67, 67, 66, 67, 61, 65, 53, 14, 66, 65, 66, 67, 66, 67, 67, 67, 65, 67, 66, 64, 12, 65, 0, 66, 67, 14, 54, 45, 67, 40, 54, 774, 39, 1167, 0, 55, 46, 0, 65, 55, 65, 67, 66, 14, 0, 47, 67, 66, 56, 66, 65, 61, 65, 56, 14, 66, 65, 65, 66, 67, 66, 65, 66, 66, 67, 66, 64, 12, 67, 0, 67, 67, 14, 57, 48, 67, 40, 57, 821, 39, 919, 0, 58, 49, 0, 66, 66, 67, 59, 65, 66, 65, 58, 65, 65, 14, 66, 65, 67, 65, 61, 65, 59, 14, 67, 65, 67, 65, 67, 66, 67, 67, 65, 65, 67, 64, 12, 65, 0, 65, 67, 14, 60, 50, 67, 40, 60, 870, 39, 1108, 0, 61, 51, 0, 65, 61, 67, 65, 65, 14, 0, 52, 66, 66, 62, 67, 67, 61, 65, 62, 14, 67, 66, 66, 67, 67, 65, 65, 67, 67, 67, 65, 64, 12, 65, 0, 66, 67, 14, 63, 53, 67, 40, 63, 917, 39, 1049, 39, 147, 2, 68, 54, 0, 0, 69, 55, 0, 44, 70, 68, 1, 69, 46, 70, 1, 12, 56486, 46, 5, 0, 68, 4, 0, 65, 68, 67, 66, 66, 0, 0, 56, 66, 65, 6, 67, 67, 55, 1, 64, 13171, 65, 64, 67, 67, 66, 12, 0, 57, 67, 67, 12, 67, 67, 39, 147, 1, 64, 12959, 65, 64, 66, 66, 66, 12, 0, 58, 66, 66, 12, 65, 67, 39, 147, 1, 12, 15689, 45, 5, 4, 2, 59, 1271, 4, 5, 934, 54, 1227, 3, 1, 64, 43391, 66, 65, 66, 12, 65, 65, 65, 64, 65, 65, 12, 65, 65, 67, 65, 39, 147, 55, 1, 64, 23591, 66, 66, 66, 12, 66, 66, 66, 64, 66, 66, 12, 66, 66, 66, 66, 39, 147, 61, 65, 6, 5, 66, 65, 66, 66, 65, 67, 66, 66, 66, 66, 66, 1, 4, 993, 55, 1, 64, 25557, 65, 64, 65, 66, 66, 12, 0, 59, 65, 67, 12, 65, 66, 39, 147, 55, 1, 4, 1167, 1, 64, 50295, 65, 64, 67, 65, 65, 12, 0, 60, 66, 66, 12, 66, 66, 39, 147, 0, 68, 12, 0, 67, 69, 66, 67, 65, 65, 66, 66, 68, 0, 66, 67, 65, 67, 67, 67, 41, 69, 1149, 1, 64, 12848, 65, 64, 65, 66, 65, 12, 0, 61, 66, 66, 12, 67, 65, 39, 147, 1, 64, 8980, 65, 64, 67, 67, 66, 12, 0, 62, 66, 66, 12, 66, 66, 39, 147, 1, 12, 2846, 0, 68, 7, 0, 45, 68, 9, 7, 8, 9, 58, 4, 1, 64, 12600, 65, 64, 67, 65, 67, 12, 0, 63, 66, 67, 12, 66, 67, 39, 147, 1, 12, 16686, 45, 5, 1, 4, 1200, 55, 1, 64, 1544, 65, 64, 66, 67, 66, 12, 0, 64, 66, 66, 12, 66, 66, 39, 147, 1, 12, 441, 0, 68, 65, 0, 8, 69, 3, 68, 61, 67, 69, 5, 66, 66, 67, 67, 66, 65, 67, 66, 66, 67, 66, 1, 64, 61104, 65, 64, 65, 67, 66, 12, 0, 66, 67, 66, 12, 66, 67, 39, 147, 1, 12, 63089, 2, 7, 2, 0, 0, 8, 6, 0, 0, 9, 67, 0, 1, 64, 19203, 66, 67, 67, 12, 65, 67, 67, 64, 65, 66, 12, 67, 67, 65, 67, 39, 147]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_8_1_0_2_10_5_9_7_11_12_3_4_13_6 (order: [8,1,0,2,10,5,9,7,11,12,3,4,13,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[10], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[13], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[12]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // GT + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a > regs[base + _operands[4]]; + // LTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a <= regs[base + _operands[7]]; + // MUL + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a * regs[base + _operands[10]]; + // SUB + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a - regs[base + _operands[13]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_4_5_6_7_2_0_3_1 (order: [4,5,6,7,2,0,3,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[3]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[3]; + var count = _operands[4]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[5]]; + regs[base + dst] = elems; + // UNARY_NEG + var dst = _operands[6]; + regs[base + dst] = -regs[base + _operands[7]]; + break; + } + case 63: + { + // ANTI_NEQ_8_1_0_3_6_2_5_4_7 (order: [8,1,0,3,6,2,5,4,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[0]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // NEQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a !== regs[base + _operands[8]]; + break; + } + case 64: + { + // ANTI_EQ_7_1_5_2_3_8_6_4_0 (order: [7,1,5,2,3,8,6,4,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[5]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[2]]); + // LOAD_CONST + var dst = _operands[3]; + regs[base + dst] = this._constant(_operands[4], _operands[5]); + // EQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a === regs[base + _operands[8]]; + break; + } + case 65: + { + // ANTI_ADD_5_10_6_8_4_2_1_7_0_3_9_11 (order: [5,10,6,8,4,2,1,7,0,3,9,11]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[11]]; + // LOAD_CONST + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // SUB + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a - regs[base + _operands[8]]; + // SUB + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a - regs[base + _operands[11]]; + break; + } + case 66: + { + // ANTI_SUB_1_6_11_4_8_2_13_10_7_12_5_0_9_3 (order: [1,6,11,4,8,2,13,10,7,12,5,0,9,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[11], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[13], _unsortedOperands[3], _unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[12], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[6]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // LOOSE_NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a != regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + // UNARY_POS + var dst = _operands[9]; + regs[base + dst] = +regs[base + _operands[10]]; + // SUB + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a - regs[base + _operands[13]]; + break; + } + case 67: + { + // ANTI_LT_6_2_1_4_5_0_12_8_7_10_11_3_9_13_14 (order: [6,2,1,4,5,0,12,8,7,10,11,3,9,13,14]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[11], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[12], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[6], _unsortedOperands[13], _unsortedOperands[14]]; + // SHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >> regs[base + _operands[2]]; + // LTE + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a <= regs[base + _operands[5]]; + // LT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a < regs[base + _operands[8]]; + // BXOR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a ^ regs[base + _operands[11]]; + // MOD + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a % regs[base + _operands[14]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/qualification.json new file mode 100644 index 00000000..21894faa --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/qualification.json @@ -0,0 +1,73 @@ +{ + "id": "exceptions--controlFlowFlatteningAntiInstrumentation--81", + "case": "exceptions", + "profile": "controlFlowFlatteningAntiInstrumentation", + "seed": 81, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "controlFlowFlattening": true, + "antiInstrumentation": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-anti-instrumentation-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/cff-branches/encoded.js b/test/vm/jsconfuser-vm/fixtures/cff-branches/encoded.js new file mode 100644 index 00000000..06207d5a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-branches/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */55772, /* 1 */0, /* 2 */21679, /* 3 */46497, /* 4 */8004, /* 5 */1333, /* 6 */4077, /* 7 */27675, /* 8 */23536, /* 9 */29143, /* 10 */22855, /* 11 */31693, /* 12 */2120, /* 13 */8229, /* 14 */24347, /* 15 */11817, /* 16 */undefined, /* 17 */2, /* 18 */"window", /* 19 */"TEST_OUTPUT", /* 20 */1, /* 21 */4]; +var BYTECODE = [4, 5, 0, 6, 0, 0, 5, 7, 6, 0, 8, 1, 0, 5, 9, 8, 0, 10, 2, 0, 27, 11, 7, 10, 40, 11, 285, 0, 12, 3, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 43, 39, 392, 0, 14, 4, 0, 11, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 63, 39, 285, 0, 17, 5, 0, 11, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 83, 39, 348, 0, 20, 6, 0, 11, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 103, 39, 466, 0, 23, 7, 0, 12, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 123, 39, 420, 0, 26, 8, 0, 11, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 143, 39, 486, 0, 29, 9, 0, 12, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 163, 39, 320, 0, 32, 10, 0, 12, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 183, 39, 404, 0, 35, 11, 0, 11, 36, 9, 35, 5, 9, 36, 26, 37, 7, 9, 40, 37, 203, 39, 450, 0, 38, 12, 0, 12, 39, 9, 38, 5, 9, 39, 26, 40, 7, 9, 40, 40, 223, 39, 504, 0, 41, 13, 0, 11, 42, 9, 41, 5, 9, 42, 26, 43, 7, 9, 40, 43, 243, 39, 291, 0, 44, 14, 0, 12, 45, 9, 44, 5, 9, 45, 26, 46, 7, 9, 40, 46, 263, 39, 364, 0, 47, 15, 0, 11, 48, 9, 47, 5, 9, 48, 26, 49, 7, 9, 40, 49, 283, 39, 429, 39, 16, 0, 51, 16, 0, 45, 51, 0, 51, 17, 0, 15, 52, 3, 51, 41, 52, 311, 1, 50, 14258, 11, 7, 7, 50, 39, 16, 1, 50, 6109, 12, 7, 7, 50, 39, 16, 1, 50, 35562, 22, 50, 7, 50, 41, 50, 339, 1, 50, 29205, 11, 7, 7, 50, 39, 16, 1, 50, 29143, 11, 7, 7, 50, 39, 16, 12, 51, 2, 3, 5, 2, 51, 1, 50, 23598, 12, 7, 7, 50, 39, 16, 1, 50, 15216, 22, 50, 7, 50, 41, 50, 383, 1, 50, 37272, 11, 7, 7, 50, 39, 16, 1, 50, 38605, 11, 7, 7, 50, 39, 16, 5, 3, 4, 1, 50, 13150, 12, 7, 7, 50, 39, 16, 0, 51, 1, 0, 5, 3, 51, 1, 50, 29573, 11, 7, 7, 50, 39, 16, 1, 50, 27675, 11, 7, 7, 50, 39, 16, 2, 51, 18, 0, 0, 52, 19, 0, 9, 51, 52, 2, 1, 50, 25455, 11, 7, 7, 50, 39, 16, 11, 51, 2, 3, 5, 2, 51, 1, 50, 3231, 12, 7, 7, 50, 39, 16, 5, 51, 3, 0, 51, 20, 0, 11, 4, 3, 51, 1, 50, 13414, 12, 7, 7, 50, 39, 16, 4, 1, 0, 51, 1, 0, 5, 2, 51, 1, 50, 51998, 12, 7, 7, 50, 39, 16, 0, 51, 21, 0, 22, 52, 3, 51, 41, 52, 524, 1, 50, 4301, 12, 7, 7, 50, 39, 16, 1, 50, 8229, 11, 7, 7, 50, 39, 16]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 53; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/cff-branches/qualification.json b/test/vm/jsconfuser-vm/fixtures/cff-branches/qualification.json new file mode 100644 index 00000000..22d1d360 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-branches/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "branches--controlFlowFlattening--41", + "case": "branches", + "profile": "controlFlowFlattening", + "seed": 41, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 2 + }, + "encodedObservation": { + "kind": "value", + "value": 2 + }, + "decodedObservation": { + "kind": "value", + "value": 2 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/cff-branches/source.js b/test/vm/jsconfuser-vm/fixtures/cff-branches/source.js new file mode 100644 index 00000000..a2f57916 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-branches/source.js @@ -0,0 +1 @@ +let value = 0; for (let i = 0; i < 4; i++) { if (i % 2) value += i; else value -= i; } window.TEST_OUTPUT = value; diff --git a/test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/encoded.js b/test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/encoded.js new file mode 100644 index 00000000..5e1257a2 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */4, /* 4 */undefined, /* 5 */35924, /* 6 */34048, /* 7 */57722, /* 8 */44575, /* 9 */18946, /* 10 */4490, /* 11 */18815, /* 12 */20709, /* 13 */22833, /* 14 */33048, /* 15 */1617, /* 16 */10488, /* 17 */98, /* 18 */7774, /* 19 */7280, /* 20 */1, /* 21 */"l0zxd", /* 22 */16, /* 23 */"Math", /* 24 */"imul", /* 25 */1102316811, /* 26 */28]; +var BYTECODE = [4, 1, 47, 4, 53, 1, 62, 1, 0, 1, 2, 5, 3, 4, 0, 4, 0, 0, 5, 2, 4, 2, 4, 1, 0, 0, 5, 2, 0, 0, 6, 3, 0, 42, 7, 3, 1, 6, 48, 6, 2, 7, 2, 9, 4, 5, 6, 0, 4, 4, 0, 45, 4, 47, 57, 1288, 3, 10, 0, 0, 4, 8, 0, 9, 5, 0, 5, 10, 9, 0, 11, 0, 0, 5, 12, 11, 0, 13, 6, 0, 27, 14, 10, 13, 34, 59, 14, 33, 59, 59, 1, 54, 1419856675, 1, 60, 927589432, 12, 60, 60, 54, 13, 60, 60, 59, 11, 54, 54, 60, 1, 55, 61181, 1, 56, 49845, 39, 1271, 0, 15, 7, 0, 5, 12, 15, 26, 16, 10, 12, 34, 59, 16, 33, 59, 59, 32, 59, 59, 1, 54, 566894523, 1, 60, 2471049450, 12, 60, 60, 54, 16, 60, 60, 59, 11, 54, 54, 60, 1, 55, 2494, 1, 56, 29310, 39, 1271, 1, 54, 3115901473, 1, 55, 26063, 1, 56, 14660, 39, 1271, 0, 17, 8, 0, 12, 18, 12, 17, 5, 12, 18, 26, 19, 10, 12, 34, 59, 19, 33, 59, 59, 32, 59, 59, 1, 54, 3760039865, 1, 60, 154887956, 12, 60, 60, 54, 16, 60, 60, 59, 11, 54, 54, 60, 1, 55, 55255, 1, 56, 24148, 39, 1271, 1, 54, 3125987376, 1, 55, 37773, 1, 56, 53995, 39, 1271, 0, 20, 9, 0, 11, 21, 12, 20, 5, 12, 21, 26, 22, 10, 12, 34, 59, 22, 33, 59, 59, 32, 59, 59, 1, 54, 1638017030, 1, 60, 3948487847, 12, 60, 60, 54, 16, 60, 59, 60, 11, 54, 54, 60, 1, 55, 56420, 1, 56, 18603, 39, 1271, 1, 54, 2009276027, 1, 55, 48946, 1, 56, 57386, 39, 1271, 0, 23, 10, 0, 12, 24, 12, 23, 5, 12, 24, 26, 25, 10, 12, 34, 59, 25, 33, 59, 59, 1, 54, 1001552498, 1, 60, 2825365873, 12, 60, 60, 54, 13, 60, 60, 59, 11, 54, 54, 60, 1, 55, 33617, 1, 56, 22763, 39, 1271, 1, 54, 970407625, 1, 55, 43322, 1, 56, 8321, 39, 1271, 0, 26, 11, 0, 11, 27, 12, 26, 5, 12, 27, 26, 28, 10, 12, 34, 59, 28, 33, 59, 59, 1, 54, 4019853989, 1, 60, 3816696044, 12, 60, 60, 54, 13, 60, 59, 60, 11, 54, 54, 60, 1, 55, 25790, 1, 56, 12508, 39, 1271, 1, 54, 823676627, 1, 55, 35595, 1, 56, 54949, 39, 1271, 0, 29, 12, 0, 12, 30, 12, 29, 5, 12, 30, 26, 31, 10, 12, 34, 59, 31, 33, 59, 59, 32, 59, 59, 1, 54, 4235783666, 1, 60, 4032625721, 12, 60, 60, 54, 16, 60, 60, 59, 11, 54, 54, 60, 1, 55, 64074, 1, 56, 14259, 39, 1271, 1, 54, 1856600451, 1, 55, 53319, 1, 56, 1399, 39, 1271, 0, 32, 13, 0, 12, 33, 12, 32, 5, 12, 33, 26, 34, 10, 12, 34, 59, 34, 33, 59, 59, 32, 59, 59, 1, 54, 4117799995, 1, 60, 1726987626, 12, 60, 60, 54, 16, 60, 59, 60, 11, 54, 54, 60, 1, 55, 15526, 1, 56, 30012, 39, 1271, 1, 54, 991390527, 1, 55, 57208, 1, 56, 43033, 39, 1271, 0, 35, 14, 0, 11, 36, 12, 35, 5, 12, 36, 26, 37, 10, 12, 34, 59, 37, 33, 59, 59, 32, 59, 59, 1, 54, 1608862041, 1, 60, 3919332858, 12, 60, 60, 54, 16, 60, 59, 60, 11, 54, 54, 60, 1, 55, 60186, 1, 56, 1680, 39, 1271, 1, 54, 190768874, 1, 55, 34517, 1, 56, 24530, 39, 1271, 0, 38, 15, 0, 11, 39, 12, 38, 5, 12, 39, 26, 40, 10, 12, 34, 59, 40, 33, 59, 59, 32, 59, 59, 1, 54, 2469713626, 1, 60, 485217147, 12, 60, 60, 54, 16, 60, 59, 60, 11, 54, 54, 60, 1, 55, 34028, 1, 56, 2163, 39, 1271, 1, 54, 2694460640, 1, 55, 44836, 1, 56, 14556, 39, 1271, 0, 41, 16, 0, 11, 42, 12, 41, 5, 12, 42, 26, 43, 10, 12, 34, 59, 43, 33, 59, 59, 32, 59, 59, 1, 54, 2336743456, 1, 60, 4160556831, 12, 60, 60, 54, 16, 60, 60, 59, 11, 54, 54, 60, 1, 55, 18722, 1, 56, 23467, 39, 1271, 1, 54, 4234710150, 1, 55, 45268, 1, 56, 63784, 39, 1271, 0, 44, 17, 0, 12, 45, 12, 44, 5, 12, 45, 26, 46, 10, 12, 34, 59, 46, 33, 59, 59, 1, 54, 867798298, 1, 60, 17299807, 12, 60, 60, 54, 13, 60, 59, 60, 11, 54, 54, 60, 1, 55, 39461, 1, 56, 60086, 39, 1271, 1, 54, 3190155236, 1, 55, 29540, 1, 56, 6956, 39, 1271, 0, 47, 18, 0, 11, 48, 12, 47, 5, 12, 48, 26, 49, 10, 12, 34, 59, 49, 33, 59, 59, 32, 59, 59, 1, 54, 2307770928, 1, 60, 2510928873, 12, 60, 60, 54, 16, 60, 59, 60, 11, 54, 54, 60, 1, 55, 43082, 1, 56, 18814, 39, 1271, 1, 54, 2310205757, 1, 55, 38999, 1, 56, 50942, 39, 1271, 0, 50, 19, 0, 12, 51, 12, 50, 5, 12, 51, 26, 52, 10, 12, 34, 59, 52, 33, 59, 59, 32, 59, 59, 1, 54, 4138659868, 1, 60, 3774818819, 12, 60, 60, 54, 16, 60, 59, 60, 11, 54, 54, 60, 1, 55, 58792, 1, 56, 13720, 39, 1271, 1, 54, 2978742873, 1, 55, 57200, 1, 56, 56919, 39, 1271, 1, 54, 1984346393, 1, 55, 30302, 1, 56, 14063, 39, 1271, 1, 10, 47931, 46, 4, 1, 53, 10884, 12, 10, 10, 53, 1, 54, 3866448608, 1, 55, 62713, 1, 56, 39132, 39, 1271, 11, 61, 6, 7, 7, 0, 61, 58, 3, 1, 53, 22222, 11, 10, 10, 53, 1, 54, 2535331114, 1, 55, 49190, 1, 56, 57840, 39, 1271, 1, 10, 2876, 45, 4, 4, 2, 59, 1109, 3, 4, 907, 0, 5, 20, 0, 1, 53, 1617, 11, 10, 10, 53, 1, 54, 1417435361, 1, 55, 15361, 1, 56, 61436, 39, 1271, 1, 53, 15936, 11, 10, 10, 53, 1, 54, 660329069, 1, 55, 12544, 1, 56, 24422, 39, 1271, 55, 1, 3, 1140, 1, 53, 11304, 11, 10, 10, 53, 1, 54, 757072685, 1, 55, 2366, 1, 56, 31432, 39, 1271, 1, 53, 2322, 22, 53, 10, 53, 34, 59, 53, 33, 59, 59, 32, 59, 59, 1, 54, 1594250783, 1, 60, 3295247765, 12, 60, 60, 54, 16, 60, 60, 59, 11, 54, 54, 60, 1, 55, 22434, 1, 56, 58228, 39, 1271, 1, 53, 34882, 12, 10, 10, 53, 1, 54, 143417251, 1, 55, 20568, 1, 56, 65334, 39, 1271, 1, 53, 396, 11, 10, 10, 53, 1, 54, 3578754081, 1, 55, 42951, 1, 56, 406, 39, 1271, 1, 10, 57722, 3, 6, 0, 5, 61, 6, 0, 7, 20, 0, 1, 53, 32013, 12, 10, 10, 53, 1, 54, 1088714793, 1, 55, 64529, 1, 56, 26545, 39, 1271, 1, 10, 13147, 0, 61, 4, 0, 45, 61, 11, 61, 0, 5, 5, 4, 61, 1, 3, 957, 1, 53, 9938, 12, 10, 10, 53, 1, 54, 2902982915, 1, 55, 8046, 1, 56, 21037, 39, 1271, 55, 1, 53, 30119, 11, 10, 10, 53, 1, 54, 1832855861, 1, 55, 54180, 1, 56, 40316, 39, 1271, 1, 53, 56834, 22, 53, 10, 53, 34, 59, 53, 33, 59, 59, 1, 54, 3363313715, 1, 60, 2876656273, 12, 60, 60, 54, 13, 60, 59, 60, 11, 54, 54, 60, 1, 55, 4892, 1, 56, 11826, 39, 1271, 1, 53, 0, 11, 10, 10, 53, 1, 54, 1962119843, 1, 55, 33113, 1, 56, 34724, 39, 1271, 1, 53, 19781, 12, 10, 10, 53, 1, 54, 2599147756, 1, 55, 62331, 1, 56, 63234, 39, 1271, 42, 54, 57, 3, 54, 55, 56, 0, 58, 21, 0, 8, 54, 54, 58, 58, 54, 4, 4, 0, 6, 22, 0, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 0, 0, 21, 8, 6, 7, 5, 5, 8, 35, 6, 0, 0, 7, 0, 0, 21, 8, 6, 7, 5, 0, 8, 2, 6, 23, 0, 0, 7, 24, 0, 8, 8, 6, 7, 0, 7, 25, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 0, 0, 21, 7, 9, 6, 5, 0, 7, 0, 6, 26, 0, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 0, 0, 21, 8, 6, 7, 5, 0, 8, 11, 6, 0, 5, 0, 7, 0, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 21, 0, 49, 7, 1, 6, 0, 45, 7, 0, 6, 4, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/qualification.json b/test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/qualification.json new file mode 100644 index 00000000..0a4c46d3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/qualification.json @@ -0,0 +1,70 @@ +{ + "id": "tryFinally--controlFlowFlatteningDispatcher--61", + "case": "tryFinally", + "profile": "controlFlowFlatteningDispatcher", + "seed": 61, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "controlFlowFlattening": true, + "dispatcher": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/source.js b/test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/source.js new file mode 100644 index 00000000..747d80bf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-dispatcher-try-finally/source.js @@ -0,0 +1 @@ +let count = 0; function check(value) { try { return value + 1; } finally { count++; } } window.TEST_OUTPUT = [check(4), count]; diff --git a/test/vm/jsconfuser-vm/fixtures/cff-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/cff-exceptions/encoded.js new file mode 100644 index 00000000..421896be --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-exceptions/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */"finished", /* 5 */undefined, /* 6 */55772, /* 7 */0, /* 8 */13936, /* 9 */3774, /* 10 */13653, /* 11 */45269, /* 12 */33650, /* 13 */25455, /* 14 */8004, /* 15 */24116, /* 16 */4248, /* 17 */4950, /* 18 */10557, /* 19 */23536, /* 20 */2715, /* 21 */2777, /* 22 */22487, /* 23 */2120, /* 24 */6109, /* 25 */18335, /* 26 */"message", /* 27 */true, /* 28 */"Error", /* 29 */"negative"]; +var BYTECODE = [4, 1, 47, 3, 69, 1, 71, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 32, 6, 5, 42, 5, 2, 1, 6, 0, 6, 3, 0, 42, 7, 2, 1, 6, 2, 6, 0, 0, 0, 8, 4, 0, 8, 9, 6, 8, 48, 6, 3, 5, 7, 9, 9, 3, 4, 6, 0, 3, 5, 0, 45, 3, 4, 10, 0, 11, 6, 0, 5, 12, 11, 0, 13, 7, 0, 5, 14, 13, 0, 15, 8, 0, 27, 16, 12, 15, 40, 16, 434, 0, 17, 9, 0, 5, 14, 17, 26, 18, 12, 14, 40, 18, 112, 39, 493, 0, 19, 10, 0, 11, 20, 14, 19, 5, 14, 20, 26, 21, 12, 14, 40, 21, 132, 39, 498, 0, 22, 11, 0, 11, 23, 14, 22, 5, 14, 23, 26, 24, 12, 14, 40, 24, 152, 39, 677, 0, 25, 12, 0, 12, 26, 14, 25, 5, 14, 26, 26, 27, 12, 14, 40, 27, 172, 39, 555, 0, 28, 13, 0, 11, 29, 14, 28, 5, 14, 29, 26, 30, 12, 14, 40, 30, 192, 39, 640, 0, 31, 14, 0, 12, 32, 14, 31, 5, 14, 32, 26, 33, 12, 14, 40, 33, 212, 39, 588, 0, 34, 15, 0, 12, 35, 14, 34, 5, 14, 35, 26, 36, 12, 14, 40, 36, 232, 39, 467, 0, 37, 16, 0, 11, 38, 14, 37, 5, 14, 38, 26, 39, 12, 14, 40, 39, 252, 39, 434, 0, 40, 17, 0, 12, 41, 14, 40, 5, 14, 41, 26, 42, 12, 14, 40, 42, 272, 39, 526, 0, 43, 18, 0, 11, 44, 14, 43, 5, 14, 44, 26, 45, 12, 14, 40, 45, 292, 39, 664, 0, 46, 19, 0, 11, 47, 14, 46, 5, 14, 47, 26, 48, 12, 14, 40, 48, 312, 39, 507, 0, 49, 20, 0, 12, 50, 14, 49, 5, 14, 50, 26, 51, 12, 14, 40, 51, 332, 39, 560, 0, 52, 21, 0, 11, 53, 14, 52, 5, 14, 53, 26, 54, 12, 14, 40, 54, 352, 39, 593, 0, 55, 22, 0, 12, 56, 14, 55, 5, 14, 56, 26, 57, 12, 14, 40, 57, 372, 39, 649, 0, 58, 23, 0, 11, 59, 14, 58, 5, 14, 59, 26, 60, 12, 14, 40, 60, 392, 39, 483, 0, 61, 24, 0, 11, 62, 14, 61, 5, 14, 62, 26, 63, 12, 14, 40, 63, 412, 39, 449, 0, 64, 25, 0, 11, 65, 14, 64, 5, 14, 65, 26, 66, 12, 14, 40, 66, 432, 39, 616, 39, 85, 9, 7, 8, 9, 58, 4, 1, 67, 2417, 11, 12, 12, 67, 39, 85, 0, 68, 2, 0, 11, 6, 0, 68, 55, 1, 67, 19195, 12, 12, 12, 67, 39, 85, 5, 5, 6, 1, 4, 493, 55, 1, 67, 37530, 11, 12, 12, 67, 39, 85, 55, 1, 67, 3231, 12, 12, 12, 67, 39, 85, 1, 12, 3774, 45, 5, 1, 67, 38345, 11, 12, 12, 67, 39, 85, 4, 2, 59, 616, 4, 5, 555, 54, 593, 3, 1, 67, 34093, 12, 12, 12, 67, 39, 85, 0, 68, 7, 0, 22, 69, 0, 68, 41, 69, 546, 1, 67, 19897, 11, 12, 12, 67, 39, 85, 1, 67, 11668, 11, 12, 12, 67, 39, 85, 1, 12, 29046, 46, 5, 1, 67, 50527, 22, 67, 12, 67, 41, 67, 579, 1, 67, 2715, 11, 12, 12, 67, 39, 85, 1, 67, 19710, 12, 12, 12, 67, 39, 85, 1, 12, 46497, 45, 5, 1, 12, 55834, 0, 68, 26, 0, 8, 69, 3, 68, 5, 5, 69, 1, 67, 6862, 11, 12, 12, 67, 39, 85, 1, 12, 59911, 2, 7, 0, 0, 0, 8, 4, 0, 0, 9, 27, 0, 1, 67, 33282, 12, 12, 12, 67, 39, 85, 1, 12, 54501, 0, 68, 5, 0, 45, 68, 2, 68, 28, 0, 0, 69, 29, 0, 44, 70, 68, 1, 69, 46, 70, 55, 1, 4, 640, 1, 67, 27675, 11, 12, 12, 67, 39, 85, 1, 4, 588, 55, 1, 67, 2785, 12, 12, 12, 67, 39, 85]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/cff-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/cff-exceptions/qualification.json new file mode 100644 index 00000000..b477375a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-exceptions/qualification.json @@ -0,0 +1,69 @@ +{ + "id": "exceptions--controlFlowFlattening--41", + "case": "exceptions", + "profile": "controlFlowFlattening", + "seed": 41, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/cff-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/cff-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/cff-for-in/encoded.js b/test/vm/jsconfuser-vm/fixtures/cff-for-in/encoded.js new file mode 100644 index 00000000..0b412c5f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-for-in/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */55772, /* 1 */0, /* 2 */3774, /* 3 */21679, /* 4 */7743, /* 5 */45975, /* 6 */30865, /* 7 */12530, /* 8 */14196, /* 9 */9024, /* 10 */11281, /* 11 */2120, /* 12 */22487, /* 13 */23598, /* 14 */30460, /* 15 */undefined, /* 16 */"b", /* 17 */5, /* 18 */"window", /* 19 */"TEST_OUTPUT", /* 20 */"a", /* 21 */2]; +var BYTECODE = [4, 10, 0, 11, 0, 0, 5, 12, 11, 0, 13, 1, 0, 5, 14, 13, 0, 15, 2, 0, 27, 16, 12, 15, 40, 16, 265, 0, 17, 3, 0, 5, 14, 17, 26, 18, 12, 14, 40, 18, 43, 39, 302, 0, 19, 4, 0, 12, 20, 14, 19, 5, 14, 20, 26, 21, 12, 14, 40, 21, 63, 39, 274, 0, 22, 5, 0, 11, 23, 14, 22, 5, 14, 23, 26, 24, 12, 14, 40, 24, 83, 39, 350, 0, 25, 6, 0, 12, 26, 14, 25, 5, 14, 26, 26, 27, 12, 14, 40, 27, 103, 39, 320, 0, 28, 7, 0, 11, 29, 14, 28, 5, 14, 29, 26, 30, 12, 14, 40, 30, 123, 39, 398, 0, 31, 8, 0, 11, 32, 14, 31, 5, 14, 32, 26, 33, 12, 14, 40, 33, 143, 39, 422, 0, 34, 9, 0, 12, 35, 14, 34, 5, 14, 35, 26, 36, 12, 14, 40, 36, 163, 39, 265, 0, 37, 10, 0, 12, 38, 14, 37, 5, 14, 38, 26, 39, 12, 14, 40, 39, 183, 39, 326, 0, 40, 11, 0, 12, 41, 14, 40, 5, 14, 41, 26, 42, 12, 14, 40, 42, 203, 39, 378, 0, 43, 12, 0, 11, 44, 14, 43, 5, 14, 44, 26, 45, 12, 14, 40, 45, 223, 39, 441, 0, 46, 13, 0, 12, 47, 14, 46, 5, 14, 47, 26, 48, 12, 14, 40, 48, 243, 39, 366, 0, 49, 14, 0, 11, 50, 14, 49, 5, 14, 50, 26, 51, 12, 14, 40, 51, 263, 39, 311, 39, 16, 1, 52, 13401, 12, 12, 12, 52, 39, 16, 1, 52, 17322, 22, 52, 12, 52, 41, 52, 293, 1, 52, 41836, 11, 12, 12, 52, 39, 16, 1, 52, 19411, 11, 12, 12, 52, 39, 16, 1, 52, 25069, 11, 12, 12, 52, 39, 16, 1, 52, 15948, 12, 12, 12, 52, 39, 16, 0, 53, 15, 0, 45, 53, 0, 53, 16, 0, 0, 54, 17, 0, 49, 8, 2, 6, 7, 53, 54, 1, 52, 20367, 11, 12, 12, 52, 39, 16, 11, 53, 3, 9, 5, 3, 53, 1, 52, 26564, 12, 12, 12, 52, 39, 16, 52, 4, 2, 1, 52, 1111, 11, 12, 12, 52, 39, 16, 53, 53, 4, 398, 5, 5, 53, 8, 9, 2, 5, 1, 52, 26564, 11, 12, 12, 52, 39, 16, 1, 12, 41576, 2, 53, 18, 0, 0, 54, 19, 0, 9, 53, 54, 3, 1, 52, 12530, 12, 12, 12, 52, 39, 16, 4, 1, 0, 6, 20, 0, 0, 7, 21, 0, 1, 52, 20305, 12, 12, 12, 52, 39, 16, 5, 2, 8, 0, 53, 1, 0, 5, 3, 53, 1, 52, 23598, 12, 12, 12, 52, 39, 16]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 55; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/cff-for-in/qualification.json b/test/vm/jsconfuser-vm/fixtures/cff-for-in/qualification.json new file mode 100644 index 00000000..13cb08c3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-for-in/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "forIn--controlFlowFlattening--41", + "case": "forIn", + "profile": "controlFlowFlattening", + "seed": 41, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 7 + }, + "encodedObservation": { + "kind": "value", + "value": 7 + }, + "decodedObservation": { + "kind": "value", + "value": 7 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/cff-for-in/source.js b/test/vm/jsconfuser-vm/fixtures/cff-for-in/source.js new file mode 100644 index 00000000..061b9cce --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-for-in/source.js @@ -0,0 +1 @@ +const object = { a: 2, b: 5 }; let sum = 0; for (const key in object) sum += object[key]; window.TEST_OUTPUT = sum; diff --git a/test/vm/jsconfuser-vm/fixtures/cff-loop-return/encoded.js b/test/vm/jsconfuser-vm/fixtures/cff-loop-return/encoded.js new file mode 100644 index 00000000..c5ea4542 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-loop-return/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */5, /* 4 */0, /* 5 */2, /* 6 */undefined, /* 7 */58675, /* 8 */62699, /* 9 */48930, /* 10 */5586, /* 11 */43534, /* 12 */648, /* 13 */1146, /* 14 */20850, /* 15 */28398, /* 16 */"Error", /* 17 */"x", /* 18 */45538, /* 19 */32563, /* 20 */31933, /* 21 */21836, /* 22 */50491, /* 23 */57462, /* 24 */42671, /* 25 */2936, /* 26 */16775, /* 27 */13580, /* 28 */1872, /* 29 */11276, /* 30 */10, /* 31 */54389, /* 32 */1602, /* 33 */53079, /* 34 */24207, /* 35 */25280, /* 36 */32207, /* 37 */20541, /* 38 */51885, /* 39 */446, /* 40 */11261, /* 41 */14566, /* 42 */24553, /* 43 */32735, /* 44 */35556, /* 45 */15721, /* 46 */18751, /* 47 */4, /* 48 */37735, /* 49 */52251, /* 50 */14179, /* 51 */1825, /* 52 */18954, /* 53 */17471, /* 54 */11044, /* 55 */3227, /* 56 */34272, /* 57 */35129, /* 58 */25620, /* 59 */24976, /* 60 */24258, /* 61 */9847, /* 62 */100, /* 63 */39426, /* 64 */35455, /* 65 */25064, /* 66 */107, /* 67 */11476, /* 68 */2819, /* 69 */3878, /* 70 */4116, /* 71 */3619, /* 72 */32925, /* 73 */24040, /* 74 */6]; +var BYTECODE = [4, 1, 47, 7, 127, 1, 37, 0, 0, 5, 2, 7, 47, 7, 466, 1, 44, 0, 0, 5, 3, 7, 47, 7, 838, 0, 56, 0, 0, 5, 4, 7, 47, 7, 1335, 0, 51, 1, 0, 1, 2, 5, 5, 7, 47, 7, 1781, 1, 40, 1, 0, 1, 2, 5, 6, 7, 2, 7, 0, 0, 0, 8, 1, 0, 0, 9, 2, 0, 42, 10, 3, 1, 9, 0, 9, 3, 0, 42, 11, 3, 1, 9, 42, 9, 4, 0, 42, 12, 5, 0, 0, 13, 4, 0, 42, 14, 6, 1, 13, 0, 13, 5, 0, 42, 15, 6, 1, 13, 48, 13, 6, 10, 11, 9, 12, 14, 15, 9, 7, 8, 13, 0, 7, 6, 0, 45, 7, 4, 3, 0, 4, 7, 0, 5, 5, 4, 0, 6, 4, 0, 5, 7, 6, 0, 8, 8, 0, 27, 9, 5, 8, 40, 9, 312, 0, 10, 7, 0, 5, 7, 10, 26, 11, 5, 7, 40, 11, 170, 39, 435, 0, 12, 9, 0, 12, 13, 7, 12, 5, 7, 13, 26, 14, 5, 7, 40, 14, 190, 39, 321, 0, 15, 10, 0, 11, 16, 7, 15, 5, 7, 16, 26, 17, 5, 7, 40, 17, 210, 39, 407, 0, 18, 11, 0, 11, 19, 7, 18, 5, 7, 19, 26, 20, 5, 7, 40, 20, 230, 39, 370, 0, 21, 12, 0, 11, 22, 7, 21, 5, 7, 22, 26, 23, 5, 7, 40, 23, 250, 39, 327, 0, 24, 13, 0, 12, 25, 7, 24, 5, 7, 25, 26, 26, 5, 7, 40, 26, 270, 39, 312, 0, 27, 14, 0, 12, 28, 7, 27, 5, 7, 28, 26, 29, 5, 7, 40, 29, 290, 39, 342, 0, 30, 15, 0, 12, 31, 7, 30, 5, 7, 31, 26, 32, 5, 7, 40, 32, 310, 39, 379, 39, 143, 1, 33, 20850, 12, 5, 5, 33, 39, 143, 0, 34, 6, 0, 45, 34, 2, 34, 16, 0, 0, 35, 17, 0, 44, 36, 34, 1, 35, 46, 36, 1, 33, 55198, 22, 33, 5, 33, 41, 33, 361, 1, 33, 21996, 11, 5, 5, 33, 39, 143, 1, 33, 0, 11, 5, 5, 33, 39, 143, 1, 33, 498, 12, 5, 5, 33, 39, 143, 1, 33, 2210, 22, 33, 5, 33, 41, 33, 398, 1, 33, 28398, 11, 5, 5, 33, 39, 143, 1, 33, 49248, 11, 5, 5, 33, 39, 143, 1, 33, 51402, 22, 33, 5, 33, 41, 33, 426, 1, 33, 22186, 11, 5, 5, 33, 39, 143, 1, 33, 0, 11, 5, 5, 33, 39, 143, 4, 2, 0, 34, 5, 0, 26, 35, 0, 34, 41, 35, 457, 1, 33, 48930, 12, 5, 5, 33, 39, 143, 1, 33, 838, 11, 5, 5, 33, 39, 143, 4, 5, 0, 6, 18, 0, 5, 7, 6, 0, 8, 4, 0, 5, 9, 8, 0, 10, 19, 0, 27, 11, 7, 10, 40, 11, 691, 0, 12, 20, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 509, 39, 829, 0, 14, 21, 0, 12, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 529, 39, 706, 0, 17, 22, 0, 11, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 549, 39, 691, 0, 20, 23, 0, 12, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 569, 39, 769, 0, 23, 24, 0, 11, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 589, 39, 779, 0, 26, 25, 0, 11, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 609, 39, 791, 0, 29, 26, 0, 12, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 629, 39, 740, 0, 32, 27, 0, 11, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 649, 39, 811, 0, 35, 28, 0, 12, 36, 9, 35, 5, 9, 36, 26, 37, 7, 9, 40, 37, 669, 39, 697, 0, 38, 29, 0, 12, 39, 9, 38, 5, 9, 39, 26, 40, 7, 9, 40, 40, 689, 39, 731, 39, 482, 0, 42, 6, 0, 45, 42, 1, 41, 11276, 12, 7, 7, 41, 39, 482, 22, 42, 3, 0, 41, 42, 722, 1, 41, 21836, 11, 7, 7, 41, 39, 482, 1, 41, 21861, 11, 7, 7, 41, 39, 482, 1, 41, 16343, 11, 7, 7, 41, 39, 482, 0, 42, 5, 0, 26, 43, 3, 42, 41, 43, 760, 1, 41, 432, 11, 7, 7, 41, 39, 482, 1, 41, 28832, 12, 7, 7, 41, 39, 482, 0, 42, 30, 0, 13, 43, 3, 42, 45, 43, 5, 3, 4, 1, 41, 35700, 12, 7, 7, 41, 39, 482, 5, 42, 3, 0, 42, 2, 0, 11, 4, 3, 42, 1, 41, 2936, 12, 7, 7, 41, 39, 482, 4, 2, 0, 42, 4, 0, 5, 3, 42, 1, 41, 35441, 12, 7, 7, 41, 39, 482, 0, 42, 2, 0, 32, 43, 42, 45, 43, 4, 5, 0, 6, 31, 0, 5, 7, 6, 0, 8, 4, 0, 5, 9, 8, 0, 10, 32, 0, 27, 11, 7, 10, 40, 11, 1143, 0, 12, 33, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 881, 39, 1199, 0, 14, 34, 0, 12, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 901, 39, 1243, 0, 17, 35, 0, 12, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 921, 39, 1276, 0, 20, 36, 0, 11, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 941, 39, 1317, 0, 23, 37, 0, 11, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 961, 39, 1188, 0, 26, 38, 0, 12, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 981, 39, 1326, 0, 29, 39, 0, 12, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 1001, 39, 1197, 0, 32, 40, 0, 11, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 1021, 39, 1270, 0, 35, 41, 0, 11, 36, 9, 35, 5, 9, 36, 26, 37, 7, 9, 40, 37, 1041, 39, 1288, 0, 38, 42, 0, 11, 39, 9, 38, 5, 9, 39, 26, 40, 7, 9, 40, 40, 1061, 39, 1252, 0, 41, 43, 0, 12, 42, 9, 41, 5, 9, 42, 26, 43, 7, 9, 40, 43, 1081, 39, 1215, 0, 44, 44, 0, 11, 45, 9, 44, 5, 9, 45, 26, 46, 7, 9, 40, 46, 1101, 39, 1172, 0, 47, 45, 0, 12, 48, 9, 47, 5, 9, 48, 26, 49, 7, 9, 40, 49, 1121, 39, 1308, 0, 50, 46, 0, 11, 51, 9, 50, 5, 9, 51, 26, 52, 7, 9, 40, 52, 1141, 39, 1143, 39, 854, 0, 54, 47, 0, 25, 55, 3, 54, 41, 55, 1163, 1, 53, 7161, 12, 7, 7, 53, 39, 854, 1, 53, 55785, 12, 7, 7, 53, 39, 854, 0, 54, 4, 0, 5, 3, 54, 1, 53, 3030, 11, 7, 7, 53, 39, 854, 1, 53, 34686, 12, 7, 7, 53, 39, 854, 45, 2, 11, 54, 2, 3, 5, 2, 54, 1, 53, 23243, 12, 7, 7, 53, 39, 854, 1, 53, 42004, 22, 53, 7, 53, 41, 53, 1234, 1, 53, 17645, 12, 7, 7, 53, 39, 854, 1, 53, 32735, 11, 7, 7, 53, 39, 854, 1, 53, 0, 11, 7, 7, 53, 39, 854, 4, 1, 0, 54, 4, 0, 5, 2, 54, 1, 53, 2821, 11, 7, 7, 53, 39, 854, 0, 54, 6, 0, 45, 54, 5, 3, 4, 1, 53, 56648, 11, 7, 7, 53, 39, 854, 5, 54, 3, 0, 54, 2, 0, 11, 4, 3, 54, 1, 53, 26244, 12, 7, 7, 53, 39, 854, 1, 53, 14851, 11, 7, 7, 53, 39, 854, 1, 53, 14145, 12, 7, 7, 53, 39, 854, 1, 53, 446, 12, 7, 7, 53, 39, 854, 4, 6, 0, 7, 48, 0, 5, 8, 7, 0, 9, 4, 0, 5, 10, 9, 0, 11, 49, 0, 27, 12, 8, 11, 40, 12, 1600, 0, 13, 50, 0, 5, 10, 13, 26, 14, 8, 10, 40, 14, 1378, 39, 1654, 0, 15, 51, 0, 12, 16, 10, 15, 5, 10, 16, 26, 17, 8, 10, 40, 17, 1398, 39, 1663, 0, 18, 52, 0, 11, 19, 10, 18, 5, 10, 19, 26, 20, 8, 10, 40, 20, 1418, 39, 1619, 0, 21, 53, 0, 11, 22, 10, 21, 5, 10, 22, 26, 23, 8, 10, 40, 23, 1438, 39, 1625, 0, 24, 54, 0, 12, 25, 10, 24, 5, 10, 25, 26, 26, 8, 10, 40, 26, 1458, 39, 1704, 0, 27, 55, 0, 12, 28, 10, 27, 5, 10, 28, 26, 29, 8, 10, 40, 29, 1478, 39, 1758, 0, 30, 56, 0, 12, 31, 10, 30, 5, 10, 31, 26, 32, 8, 10, 40, 32, 1498, 39, 1742, 0, 33, 57, 0, 11, 34, 10, 33, 5, 10, 34, 26, 35, 8, 10, 40, 35, 1518, 39, 1600, 0, 36, 58, 0, 11, 37, 10, 36, 5, 10, 37, 26, 38, 8, 10, 40, 38, 1538, 39, 1672, 0, 39, 59, 0, 12, 40, 10, 39, 5, 10, 40, 26, 41, 8, 10, 40, 41, 1558, 39, 1602, 0, 42, 60, 0, 12, 43, 10, 42, 5, 10, 43, 26, 44, 8, 10, 40, 44, 1578, 39, 1684, 0, 45, 61, 0, 11, 46, 10, 45, 5, 10, 46, 26, 47, 8, 10, 40, 47, 1598, 39, 1722, 39, 1351, 45, 2, 11, 49, 2, 3, 5, 2, 49, 55, 1, 48, 21830, 12, 8, 8, 48, 39, 1351, 0, 49, 6, 0, 45, 49, 0, 49, 47, 0, 22, 50, 3, 49, 41, 50, 1645, 1, 48, 13414, 12, 8, 8, 48, 39, 1351, 1, 48, 27181, 12, 8, 8, 48, 39, 1351, 1, 48, 2428, 12, 8, 8, 48, 39, 1351, 1, 48, 25381, 11, 8, 8, 48, 39, 1351, 5, 3, 5, 1, 48, 12206, 12, 8, 8, 48, 39, 1351, 5, 49, 3, 0, 49, 2, 0, 11, 5, 3, 49, 1, 48, 49234, 11, 8, 8, 48, 39, 1351, 4, 1, 0, 49, 4, 0, 5, 2, 49, 1, 48, 37499, 12, 8, 8, 48, 39, 1351, 54, 1758, 4, 3, 49, 0, 42, 50, 49, 1, 3, 1, 48, 14411, 11, 8, 8, 48, 39, 1351, 0, 49, 4, 0, 5, 3, 49, 1, 48, 48543, 11, 8, 8, 48, 39, 1351, 1, 8, 34508, 0, 49, 62, 0, 11, 50, 2, 49, 5, 2, 50, 1, 48, 857, 11, 8, 8, 48, 39, 1351, 4, 5, 0, 6, 63, 0, 5, 7, 6, 0, 8, 4, 0, 5, 9, 8, 0, 10, 64, 0, 27, 11, 7, 10, 40, 11, 1986, 0, 12, 65, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 1824, 39, 2142, 0, 14, 66, 0, 11, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 1844, 39, 2108, 0, 17, 67, 0, 12, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 1864, 39, 2046, 0, 20, 68, 0, 12, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 1884, 39, 2136, 0, 23, 69, 0, 12, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 1904, 39, 2080, 0, 26, 70, 0, 12, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 1924, 39, 1986, 0, 29, 71, 0, 11, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 1944, 39, 2074, 0, 32, 72, 0, 11, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 1964, 39, 2001, 0, 35, 73, 0, 11, 36, 9, 35, 5, 9, 36, 26, 37, 7, 9, 40, 37, 1984, 39, 2018, 39, 1797, 42, 39, 4, 1, 0, 55, 1, 38, 7994, 11, 7, 7, 38, 39, 1797, 4, 2, 54, 2142, 3, 3, 4, 0, 1, 38, 36544, 12, 7, 7, 38, 39, 1797, 1, 38, 21371, 22, 38, 7, 38, 41, 38, 2037, 1, 38, 60584, 12, 7, 7, 38, 39, 1797, 1, 38, 56965, 12, 7, 7, 38, 39, 1797, 1, 38, 4002, 22, 38, 7, 38, 41, 38, 2065, 1, 38, 49771, 11, 7, 7, 38, 39, 1797, 1, 38, 6697, 12, 7, 7, 38, 39, 1797, 0, 39, 6, 0, 45, 39, 1, 38, 37145, 22, 38, 7, 38, 41, 38, 2099, 1, 38, 3878, 11, 7, 7, 38, 39, 1797, 1, 38, 56468, 11, 7, 7, 38, 39, 1797, 1, 38, 55748, 22, 38, 7, 38, 41, 38, 2127, 1, 38, 18670, 12, 7, 7, 38, 39, 1797, 1, 38, 14295, 12, 7, 7, 38, 39, 1797, 0, 39, 74, 0, 45, 39, 1, 7, 25064, 0, 39, 3, 0, 45, 39]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 16; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/cff-loop-return/generation.json b/test/vm/jsconfuser-vm/fixtures/cff-loop-return/generation.json new file mode 100644 index 00000000..ac4a4a93 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-loop-return/generation.json @@ -0,0 +1,12 @@ +{ + "note": "encoded.js is the VM encoder's output for source.js with controlFlowFlattening, and plain.encoded.js with no options, each with the leading disassembly comment block removed (the committed corpus format). The CFF encode replaced Math.random with the seeded PRNG below (mulberry32: state += 0x6d2b79f5, then the standard mulberry32 output); the plain encode draws no randomness. Regenerating with the same encoder revision, options and seed reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5" + }, + "files": { + "encoded.js": { "options": { "controlFlowFlattening": true }, "seed": 324508639 }, + "plain.encoded.js": { "options": {} } + }, + "claim": "loops and tries whose exits CFF reorders: a conditional returning from its fallthrough arm, a break that is a loop's only exit, a catch ending in break, a catch that always returns. The CFF decode keeps every jump the stream does not prove a fallthrough and places each conditional's proved fallthrough after it, so it reads as the plain decode does" +} diff --git a/test/vm/jsconfuser-vm/fixtures/cff-loop-return/plain.encoded.js b/test/vm/jsconfuser-vm/fixtures/cff-loop-return/plain.encoded.js new file mode 100644 index 00000000..d30dc55a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-loop-return/plain.encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */5, /* 4 */0, /* 5 */2, /* 6 */undefined, /* 7 */"Error", /* 8 */"x", /* 9 */10, /* 10 */4, /* 11 */100, /* 12 */6]; +var BYTECODE = [4, 1, 47, 7, 127, 1, 6, 0, 0, 5, 2, 7, 47, 7, 161, 1, 6, 0, 0, 5, 3, 7, 47, 7, 229, 0, 6, 0, 0, 5, 4, 7, 47, 7, 289, 0, 7, 1, 0, 1, 2, 5, 5, 7, 47, 7, 374, 1, 6, 1, 0, 1, 2, 5, 6, 7, 2, 7, 0, 0, 0, 8, 1, 0, 0, 9, 2, 0, 42, 10, 3, 1, 9, 0, 9, 3, 0, 42, 11, 3, 1, 9, 42, 9, 4, 0, 42, 12, 5, 0, 0, 13, 4, 0, 42, 14, 6, 1, 13, 0, 13, 5, 0, 42, 15, 6, 1, 13, 48, 13, 6, 10, 11, 9, 12, 14, 15, 9, 7, 8, 13, 0, 7, 6, 0, 45, 7, 4, 2, 0, 3, 5, 0, 26, 4, 0, 3, 40, 4, 155, 2, 3, 7, 0, 0, 4, 8, 0, 44, 5, 3, 1, 4, 46, 5, 0, 3, 6, 0, 45, 3, 4, 2, 0, 4, 4, 0, 5, 3, 4, 22, 4, 3, 0, 40, 4, 214, 0, 4, 5, 0, 26, 5, 3, 4, 40, 5, 198, 0, 4, 9, 0, 13, 5, 3, 4, 45, 5, 5, 4, 3, 0, 4, 2, 0, 11, 5, 3, 4, 5, 3, 5, 39, 170, 0, 4, 2, 0, 32, 5, 4, 45, 5, 0, 4, 6, 0, 45, 4, 4, 1, 0, 4, 4, 0, 5, 2, 4, 0, 4, 4, 0, 5, 3, 4, 0, 4, 10, 0, 25, 5, 3, 4, 40, 5, 258, 39, 281, 11, 4, 2, 3, 5, 2, 4, 5, 4, 3, 0, 4, 2, 0, 11, 5, 3, 4, 5, 3, 5, 39, 245, 45, 2, 0, 4, 6, 0, 45, 4, 4, 1, 0, 5, 4, 0, 5, 2, 5, 0, 5, 4, 0, 5, 3, 5, 0, 5, 10, 0, 22, 6, 3, 5, 40, 6, 366, 54, 337, 4, 3, 5, 0, 42, 6, 5, 1, 3, 11, 5, 2, 3, 5, 2, 5, 55, 39, 350, 0, 5, 11, 0, 11, 6, 2, 5, 5, 2, 6, 39, 366, 5, 5, 3, 0, 5, 2, 0, 11, 6, 3, 5, 5, 3, 6, 39, 305, 45, 2, 0, 5, 6, 0, 45, 5, 4, 2, 54, 390, 3, 3, 4, 0, 42, 5, 4, 1, 0, 55, 39, 396, 0, 4, 3, 0, 45, 4, 0, 4, 12, 0, 45, 4, 0, 4, 6, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 16; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/cff-loop-return/source.js b/test/vm/jsconfuser-vm/fixtures/cff-loop-return/source.js new file mode 100644 index 00000000..352e2421 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-loop-return/source.js @@ -0,0 +1,6 @@ +function g(x) { if (x === 2) throw new Error("x"); } +function loopReturn(n) { for (var i = 0; i < n; i++) { if (i === 2) return i * 10; } return -1; } +function loopBreak() { var s = 0; for (var j = 0; ; j++) { if (j >= 4) break; s += j; } return s; } +function loopCatchBreak() { var n = 0; for (var i = 0; i < 4; i++) { try { g(i); n += i; } catch (e) { n += 100; break; } } return n; } +function catchReturn(x) { try { g(x); } catch (e) { return 5; } return 6; } +window.TEST_OUTPUT = [loopReturn(1), loopReturn(5), loopBreak(), loopCatchBreak(), catchReturn(0), catchReturn(2)]; diff --git a/test/vm/jsconfuser-vm/fixtures/cff-switch/encoded.js b/test/vm/jsconfuser-vm/fixtures/cff-switch/encoded.js new file mode 100644 index 00000000..a6e75f2d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-switch/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */3, /* 4 */undefined, /* 5 */55772, /* 6 */0, /* 7 */46497, /* 8 */35467, /* 9 */13086, /* 10 */37530, /* 11 */56137, /* 12 */13653, /* 13 */9202, /* 14 */5607, /* 15 */23536, /* 16 */2715, /* 17 */35828, /* 18 */16118, /* 19 */22487, /* 20 */14258, /* 21 */12530, /* 22 */"two", /* 23 */"other", /* 24 */"one", /* 25 */2]; +var BYTECODE = [4, 1, 47, 3, 53, 1, 54, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 42, 6, 2, 1, 5, 0, 5, 3, 0, 42, 7, 2, 1, 5, 48, 5, 2, 6, 7, 9, 3, 4, 5, 0, 3, 4, 0, 45, 3, 4, 3, 0, 4, 5, 0, 5, 5, 4, 0, 6, 6, 0, 5, 7, 6, 0, 8, 7, 0, 27, 9, 5, 8, 40, 9, 358, 0, 10, 8, 0, 5, 7, 10, 26, 11, 5, 7, 40, 11, 96, 39, 419, 0, 12, 9, 0, 12, 13, 7, 12, 5, 7, 13, 26, 14, 5, 7, 40, 14, 116, 39, 428, 0, 15, 10, 0, 11, 16, 7, 15, 5, 7, 16, 26, 17, 5, 7, 40, 17, 136, 39, 407, 0, 18, 11, 0, 12, 19, 7, 18, 5, 7, 19, 26, 20, 5, 7, 40, 20, 156, 39, 470, 0, 21, 12, 0, 11, 22, 7, 21, 5, 7, 22, 26, 23, 5, 7, 40, 23, 176, 39, 455, 0, 24, 13, 0, 11, 25, 7, 24, 5, 7, 25, 26, 26, 5, 7, 40, 26, 196, 39, 505, 0, 27, 14, 0, 11, 28, 7, 27, 5, 7, 28, 26, 29, 5, 7, 40, 29, 216, 39, 464, 0, 30, 15, 0, 11, 31, 7, 30, 5, 7, 31, 26, 32, 5, 7, 40, 32, 236, 39, 367, 0, 33, 16, 0, 12, 34, 7, 33, 5, 7, 34, 26, 35, 5, 7, 40, 35, 256, 39, 398, 0, 36, 17, 0, 12, 37, 7, 36, 5, 7, 37, 26, 38, 5, 7, 40, 38, 276, 39, 437, 0, 39, 18, 0, 11, 40, 7, 39, 5, 7, 40, 26, 41, 5, 7, 40, 41, 296, 39, 446, 0, 42, 19, 0, 11, 43, 7, 42, 5, 7, 43, 26, 44, 5, 7, 40, 44, 316, 39, 358, 0, 45, 20, 0, 12, 46, 7, 45, 5, 7, 46, 26, 47, 5, 7, 40, 47, 336, 39, 476, 0, 48, 21, 0, 12, 49, 7, 48, 5, 7, 49, 26, 50, 5, 7, 40, 50, 356, 39, 413, 39, 69, 1, 51, 26788, 12, 5, 5, 51, 39, 69, 4, 2, 0, 52, 2, 0, 26, 53, 0, 52, 41, 53, 389, 1, 51, 14196, 12, 5, 5, 51, 39, 69, 1, 51, 22425, 12, 5, 5, 51, 39, 69, 1, 51, 6854, 11, 5, 5, 51, 39, 69, 0, 52, 22, 0, 45, 52, 0, 52, 23, 0, 45, 52, 1, 51, 24444, 11, 5, 5, 51, 39, 69, 1, 51, 0, 11, 5, 5, 51, 39, 69, 1, 51, 0, 11, 5, 5, 51, 39, 69, 1, 51, 1111, 12, 5, 5, 51, 39, 69, 1, 51, 18040, 11, 5, 5, 51, 39, 69, 0, 52, 24, 0, 45, 52, 0, 52, 4, 0, 45, 52, 0, 52, 25, 0, 26, 53, 0, 52, 41, 53, 496, 1, 51, 14258, 11, 5, 5, 51, 39, 69, 1, 51, 6109, 12, 5, 5, 51, 39, 69, 1, 51, 50527, 22, 51, 5, 51, 41, 51, 524, 1, 51, 8838, 11, 5, 5, 51, 39, 69, 1, 51, 22855, 12, 5, 5, 51, 39, 69]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/cff-switch/qualification.json b/test/vm/jsconfuser-vm/fixtures/cff-switch/qualification.json new file mode 100644 index 00000000..97a041e7 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-switch/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "switch--controlFlowFlattening--41", + "case": "switch", + "profile": "controlFlowFlattening", + "seed": 41, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/cff-switch/source.js b/test/vm/jsconfuser-vm/fixtures/cff-switch/source.js new file mode 100644 index 00000000..98a67db9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-switch/source.js @@ -0,0 +1 @@ +function classify(value) { switch (value) { case 1: return "one"; case 2: return "two"; default: return "other"; } } window.TEST_OUTPUT = [classify(1), classify(3)]; diff --git a/test/vm/jsconfuser-vm/fixtures/cff-try-catch/encoded.js b/test/vm/jsconfuser-vm/fixtures/cff-try-catch/encoded.js new file mode 100644 index 00000000..e2589aef --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-try-catch/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */undefined, /* 5 */55772, /* 6 */0, /* 7 */29046, /* 8 */35467, /* 9 */24444, /* 10 */18335, /* 11 */14196, /* 12 */23536, /* 13 */23598, /* 14 */1333, /* 15 */21154, /* 16 */"message", /* 17 */"Error", /* 18 */"negative"]; +var BYTECODE = [4, 1, 47, 3, 56, 1, 39, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 32, 6, 5, 42, 5, 2, 1, 6, 0, 6, 3, 0, 42, 7, 2, 1, 6, 48, 6, 2, 5, 7, 9, 3, 4, 6, 0, 3, 4, 0, 45, 3, 4, 5, 0, 6, 5, 0, 5, 7, 6, 0, 8, 6, 0, 5, 9, 8, 0, 10, 7, 0, 27, 11, 7, 10, 40, 11, 241, 0, 12, 8, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 99, 39, 241, 0, 14, 9, 0, 11, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 119, 39, 302, 0, 17, 10, 0, 12, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 139, 39, 251, 0, 20, 11, 0, 11, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 159, 39, 275, 0, 23, 12, 0, 12, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 179, 39, 342, 0, 26, 13, 0, 11, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 199, 39, 262, 0, 29, 14, 0, 12, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 219, 39, 293, 0, 32, 15, 0, 12, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 239, 39, 327, 39, 72, 55, 1, 35, 3231, 12, 7, 7, 35, 39, 72, 0, 36, 2, 0, 11, 37, 0, 36, 55, 45, 37, 1, 7, 55834, 0, 36, 16, 0, 8, 37, 3, 36, 45, 37, 4, 2, 54, 262, 3, 0, 4, 6, 0, 1, 35, 4139, 11, 7, 7, 35, 39, 72, 1, 35, 1333, 11, 7, 7, 35, 39, 72, 22, 36, 0, 4, 41, 36, 318, 1, 35, 18335, 12, 7, 7, 35, 39, 72, 1, 35, 26564, 12, 7, 7, 35, 39, 72, 2, 36, 17, 0, 0, 37, 18, 0, 44, 38, 36, 1, 37, 46, 38, 0, 36, 4, 0, 45, 36]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/cff-try-catch/qualification.json b/test/vm/jsconfuser-vm/fixtures/cff-try-catch/qualification.json new file mode 100644 index 00000000..416746b9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-try-catch/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "tryCatch--controlFlowFlattening--41", + "case": "tryCatch", + "profile": "controlFlowFlattening", + "seed": 41, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/cff-try-catch/source.js b/test/vm/jsconfuser-vm/fixtures/cff-try-catch/source.js new file mode 100644 index 00000000..6403185a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-try-catch/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } } window.TEST_OUTPUT = [check(-1), check(4)]; diff --git a/test/vm/jsconfuser-vm/fixtures/cff-try-finally/encoded.js b/test/vm/jsconfuser-vm/fixtures/cff-try-finally/encoded.js new file mode 100644 index 00000000..9c3c6984 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-try-finally/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */4, /* 4 */undefined, /* 5 */55772, /* 6 */46497, /* 7 */33347, /* 8 */10966, /* 9 */13086, /* 10 */3231, /* 11 */14809, /* 12 */9202, /* 13 */22855, /* 14 */51998, /* 15 */2715, /* 16 */35828, /* 17 */42682, /* 18 */18335, /* 19 */12530, /* 20 */26788, /* 21 */1]; +var BYTECODE = [4, 1, 47, 4, 53, 1, 58, 1, 0, 1, 2, 5, 3, 4, 0, 4, 0, 0, 5, 2, 4, 2, 4, 1, 0, 0, 5, 2, 0, 0, 6, 3, 0, 42, 7, 3, 1, 6, 48, 6, 2, 7, 2, 9, 4, 5, 6, 0, 4, 4, 0, 45, 4, 4, 8, 0, 9, 5, 0, 5, 10, 9, 0, 11, 0, 0, 5, 12, 11, 0, 13, 6, 0, 27, 14, 10, 13, 40, 14, 358, 0, 15, 7, 0, 5, 12, 15, 26, 16, 10, 12, 40, 16, 96, 39, 419, 0, 17, 8, 0, 12, 18, 12, 17, 5, 12, 18, 26, 19, 10, 12, 40, 19, 116, 39, 432, 0, 20, 9, 0, 11, 21, 12, 20, 5, 12, 21, 26, 22, 10, 12, 40, 22, 136, 39, 409, 0, 23, 10, 0, 12, 24, 12, 23, 5, 12, 24, 26, 25, 10, 12, 40, 25, 156, 39, 496, 0, 26, 11, 0, 12, 27, 12, 26, 5, 12, 27, 26, 28, 10, 12, 40, 28, 176, 39, 469, 0, 29, 12, 0, 11, 30, 12, 29, 5, 12, 30, 26, 31, 10, 12, 40, 31, 196, 39, 515, 0, 32, 13, 0, 12, 33, 12, 32, 5, 12, 33, 26, 34, 10, 12, 40, 34, 216, 39, 478, 0, 35, 14, 0, 11, 36, 12, 35, 5, 12, 36, 26, 37, 10, 12, 40, 37, 236, 39, 380, 0, 38, 15, 0, 12, 39, 12, 38, 5, 12, 39, 26, 40, 10, 12, 40, 40, 256, 39, 400, 0, 41, 16, 0, 12, 42, 12, 41, 5, 12, 42, 26, 43, 10, 12, 40, 43, 276, 39, 441, 0, 44, 17, 0, 11, 45, 12, 44, 5, 12, 45, 26, 46, 10, 12, 40, 46, 296, 39, 450, 0, 47, 18, 0, 12, 48, 12, 47, 5, 12, 48, 26, 49, 10, 12, 40, 49, 316, 39, 358, 0, 50, 19, 0, 12, 51, 12, 50, 5, 12, 51, 26, 52, 10, 12, 40, 52, 336, 39, 505, 0, 53, 20, 0, 11, 54, 12, 53, 5, 12, 54, 26, 55, 10, 12, 40, 55, 356, 39, 414, 39, 69, 1, 10, 41576, 3, 6, 0, 5, 57, 6, 0, 7, 21, 0, 1, 56, 37802, 12, 10, 10, 56, 39, 69, 4, 2, 59, 358, 3, 4, 409, 0, 5, 21, 0, 1, 56, 4139, 11, 10, 10, 56, 39, 69, 1, 56, 17590, 12, 10, 10, 56, 39, 69, 1, 10, 35467, 46, 4, 1, 10, 55834, 45, 4, 55, 1, 3, 496, 1, 56, 8229, 11, 10, 10, 56, 39, 69, 1, 56, 0, 11, 10, 10, 56, 39, 69, 1, 56, 0, 11, 10, 10, 56, 39, 69, 11, 57, 0, 5, 5, 4, 57, 1, 3, 414, 1, 56, 30865, 12, 10, 10, 56, 39, 69, 1, 56, 15920, 11, 10, 10, 56, 39, 69, 11, 57, 6, 7, 7, 0, 57, 58, 3, 1, 56, 31693, 11, 10, 10, 56, 39, 69, 1, 10, 32236, 0, 57, 4, 0, 45, 57, 55, 1, 56, 12530, 11, 10, 10, 56, 39, 69, 1, 56, 50527, 22, 56, 10, 56, 41, 56, 534, 1, 56, 6718, 11, 10, 10, 56, 39, 69, 1, 56, 5607, 11, 10, 10, 56, 39, 69]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/cff-try-finally/qualification.json b/test/vm/jsconfuser-vm/fixtures/cff-try-finally/qualification.json new file mode 100644 index 00000000..d90b0260 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-try-finally/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "tryFinally--controlFlowFlattening--41", + "case": "tryFinally", + "profile": "controlFlowFlattening", + "seed": 41, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/cff-try-finally/source.js b/test/vm/jsconfuser-vm/fixtures/cff-try-finally/source.js new file mode 100644 index 00000000..747d80bf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/cff-try-finally/source.js @@ -0,0 +1 @@ +let count = 0; function check(value) { try { return value + 1; } finally { count++; } } window.TEST_OUTPUT = [check(4), count]; diff --git a/test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/encoded.js new file mode 100644 index 00000000..b56230ba --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/encoded.js @@ -0,0 +1,2500 @@ +function VM(globals, fake_13, fake_15, constants, fake_16, bytecode, fake_14) { + this.O = fake_14; + this.ag = bytecode; + this.a = constants; + this.J = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.S = fake_16; + this.I = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.f = []; + this.z = 1; + this.y = 0; + this.K = fake_15 && fake_13; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(fake_1, regs, absSlot, fake_2) { + this.f = regs; // shared reference to VM._regs flat array + this.F = absSlot; // absolute index; stable as long as frame is alive + this.x = false; + this.v = fake_1 || fake_2; + this.h = undefined; +} +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CONSTANTS = [/* 0 */-1840036532, /* 1 */-728299185, /* 2 */672545599, /* 3 */-1904726704, /* 4 */"2nTUBOKP8zuMlEltnv0xVbrU6j/JQ8bU+nyb7RUlrLQBD5+bDgekjDYd9qVXJWHfpnM2x7pcIs3JtSUllOxOe1yXTQsPvUYVaJUu/UtjMtcqs2hNT8VSdVnteltzJyKrWh9chdMFNZ2JMgSnliNibxz1aUVDzYA6KleH60B//OoDlew9JY35Fo3z9m+Sohs17E0Y3bZGVMuaX9uqxCaPnc0NyIbmAuGP3TOZprBtyf20VvPKsL/aS4zW83rI7QEWsrIeDs2DUPaZcscd16X+KtjehUs8xqJaLS6opTAije4RcriHUgJhvr4VOmq4/jVquNcNuo1OYtpLcm3uBaJ6NnyyfB4xgp91VZ5BKjCmOdprTkv6S0ZmznIyGKa/40t+2hIlhhU+X4ppFlv6ZG7l2hI2sklKwoV2FvLuXyWi+zbqkZ4J6IYMGsCvTzq8piRq2/HoRu3Cib/zU+fm8WLu6cERp7rmD9eb0PbKatgh/KnAMsjezUM595ZSISmmoTEZwY7se/WH5Qo4saspEGGd/g5ToscgQ5NOetFteXrhP5vmNzOLqx4liY1xNJkqI063RtMKT2DBIFla0Xg5YKdt63h/YAlrgWk5EZF6Z3zjTW990y05jkEQ2ZlwBesbH1SrWzFGmWMB8HgFE8qvfCOQ2DJRg/kiQfzI9L8XK4fgDXmfkU4ZsIBMD9WTn/jiYOTZ97HQSPrA8Us599JYryHeuf8QxojaA4qXwRCuuI0hP2ib8AxIocciOJxQ7Nn/YbjoBJC0Bx6AqBgE8IhpKOCIKAOwW9haQGH4t1E5qLcgC6isEyh4fwBQiGEwZ6giYCn4RlBd2DNATUgy0GVocuBbGFawQQhHgDQIoXBKGLWvODiZ0HRIcsBaSFPQq78SIIvoLXCEiXIAqLglMI+b7+DcaOnRH0iRQBjYmXMe6KBg+in3sdkY04CDe6WX2yjpoPcpHFDr+LZDwcenMPyo9+Hbeerw15i8BzOL3xA66eVh2NjdMOu36cP5SDTxkVkRofM4cZf8Cw5/sghDkVE55qAyZ7HzOU++wFG5uDFM2U1nUOtxH3GLXQFtmWzxdnlK42aveTNg2UxBeflSUXYrVL8EK6+WF3msgQAZkrJYL1djSed2cZbZKkH5ShzfrGsg56WaETGcufgC1Y6bcyyXviIDucrRN0mN/utLzteeOtuu7RnHcfkO+oLOB8GyzR7v4aZp+t7zSupHxdvQTg/6zGEgrpYyU5avA8t+yRLuqQI+76otVqb7GE642gW2rikg4nBmT/JpHrmCKQafkTXqjWYX2rqvcjp4xnpKHPJCVnAiQ75jErqWNgJzinsWYrpELlNaQvZWSnPeDka3sives2Mx5l6SWg5ahgUaEI6qewXmqSo0vqTVbEKS/htS7NfFIsqutxUseq4OGZqPNxiq6x7q5dp1i96EQt5H0NPnbsTi2GXHrq46rYe3C9F+xOrjpcQ9r9JaV4vDUE+U0jW1yi3+mup2+YsDH7CKKjX3nTfthWYR06FPD0NmxVJNBf2FZgMriL8eC4SGUH1QjR0Vf6N9L3ZTJfdSTWXdRzVuq0ffTGtr93KdTg1HhX4VZu+GYzXnqiwOvanFP0VD/01rWaeLPCStkBUfhc0MA5OjNzijrFwO9dp159wxS4Q3JNuxbCjljxUPjf8854fNE9V8wOTMpdA9uMw7R+z79UzH1OGlgC3knNx084PeH9+0VTTXnUDssHQ81NpP8UTy1IVtEfyAZCIUzL81BLGEKnyl7QYkeqRILB9Un8QETIDdVDTFpHMcH3Qd9EWcWwxRtFoUSuxDfEfUVkxTvCnEYlRf/FlsYKQgPFaMwQULhJYMNZyoJwOseFx79HJEg9wITIo3FNz1bBb0kRUejIc895fiE5p8MuTsVSA8ucxPRLP76Wz/JN2l5i2cnNIE9oPuH/O0tyXdXe3s93TZ291P2rSk1K9tO/2SlFETs79SA7WU0n2A7cYk6bTizwNb+cQRTbHdCCWppCofvHNYB1OdoQ0JtaUkXO+zexfXsExMPRnVTFUD/2RjWZdNM0KNcgVudT/veZtLJ3uraV1nxWxFWt1es5EnAqOobgr1rAVYjUE7SJcv67R+CtWuVRE9r8J3V+vzIW+GIua12A29mjAHwosHH4KqDyW/XanymUbyw95PubLqJvttpP2wlrgL57/1CuyW0H3E7d5W3bLRzxZCnPYtcpndFiadqu4fxovjBhyapy0HpqMiLu6AcwPGjUI4PgfWWmp2/oVrIpbBOgGOiBUYclzuZ6JzNkmyV15fwnNKUt4xqlYmTJo0DlD6bAIBjncye6Y04kl+ONKgSQ++pMoWVkP6cW5+GnG2IA7rgjB2qPM6HqOiADaaURrK+Ubb29cv7LogJtxpB/KThh4C/r//Etvm3GGK6d5W8brfz65b4/bWacmh4jbpsssf2YPm9tmSvClXobBaB8/dey7HmUr+MeGpl2Ix/5YTP4enAgSJpxEjebLuPVsGN1XLEEG5yR5xj94ko443CZN0CUWBRRlKkVw7Lad/60B/aMk0QTS5WtNZZ3jjZ294EXm5XwGwmAt3ges4X4qoDDF4WVHBXHek0wkv7KAAGZORd/mKgAULor/fKPDg/nk/0a1YPcPKzytTi/jXYfaZ4zD3iMYfvYv88NuY1iFHudJQ8c/eQ6HY+VCbOe+humjv8KALL4DOOAyQoBlmYPbo0lPBx6bAM1i78W5okOAxqPA3aZiOAHr4AxAaqD0gjFcc0Id4AcCLSWCwXih/YFv4RpBiGHWAIQhSkG9oOeA/WFmwaMhDQBrIH3B92C4gjrgsELOJHQCniE4QZrgEIEDon3FoyINAIDiLzxlYsuA/aIWRxAjOgOYfH3Cv6AVguykRsIRY1s/ie97QwKi9Id6p/BCk+NED2Ii1MPmpouHFeOnwzlvxx/zIGFGW6TZhsRgGs5o39YDdCNnxDBmgoDM7n1cly/9AH8mPUWe5WyNGZ33z4IhqAbi5JDHXmF1n1eswXxrJe8FBWX3xUHdq41kvarN1GTmBeflgl1orAL9lKy7RKnk6wdVGUjPDrzhj7+Z5cWWZcgE0i+wfAGuP5ndZmDEkuYFCDs/uc+DWy6KBOW6RvAoU/6kLIJfVOvehwNn9cuTOwkPox98y6l7t4eVq/h7oi/sHwJrqDvT57WHdrhkyglcuwrt+bdKYKfu++irel2L66458GqexKy6f4hBmgfM7XlTCS8ZaXu7qC2aKmxkuk7pcBneOZfJHll4iW75nUmPWYUJAylBWezpari4aWvZgCg+CegZasrcfFGK45gdSjs0NxhNad84MevHm", /* 5 */"yrAQyJFBF/iKUA7Y", /* 6 */"VeBMmEcwPoi4Awf4", /* 7 */2039942730, /* 8 */"vsCJy+Ff1OjKYPAZ4rHPOCOA2Q8=", /* 9 */1930097227, /* 10 */undefined, /* 11 */"6krc3s4r+KfLGuuOxwU=", /* 12 */-1396740206, /* 13 */477193358, /* 14 */-1241747460, /* 15 */"4Z/5C/2EFxySrQAk", /* 16 */"irMpX5LgLGmikTsJpoA=", /* 17 */-1010165680, /* 18 */"OfJIXl0iSbZyEkeOFQJPmg==", /* 19 */"6iHOucMWw4qx/wKLghYIuQ==", /* 20 */872101760, /* 21 */832451116, /* 22 */"//jFZ+Gr8TDnSA==", /* 23 */"juMPrqIyCdaqQV/6XlZaKla+", /* 24 */-622628806, /* 25 */"j7Ee2aZAD8eFU3GvYSFXmQ==", /* 26 */"zprbDfCF1AIejg==", /* 27 */"JfC7aOMn+buHEA==", /* 28 */401186932, /* 29 */"kcEAyp5eFOq/Z+Qa367GOQ==", /* 30 */"Z3Bx2F9ARkhd0GloTuAOGIyvDjg=", /* 31 */"fI1kG2WnSyuPXgr1", /* 32 */-914357643, /* 33 */"C69522xnAe1nnTkFgYICH5PzNmc=", /* 34 */829234186, /* 35 */933233088, /* 36 */350624896, /* 37 */"SH5b4nymcz4=", /* 38 */"6bUoFcFsRvuoV2PLg7wbRQ==", /* 39 */461958816, /* 40 */"GJS8Ew58pgQ7lA==", /* 41 */"WJ1gJ2ejK1+F8l5FU90hTUU3R9s=", /* 42 */-1042704316, /* 43 */"phcI44t+AdG5SVvB", /* 44 */"HUDQ2zpwtegqoLYpFbA=", /* 45 */833860180, /* 46 */"9Mo6v7gr", /* 47 */1936874945, /* 48 */"egRKfGsMT5R0PH+sTFRG9I9MMNw=", /* 49 */-2126286310, /* 50 */"7J3OBbP0Fx+pqwQn", /* 51 */"vWd5IGW4fxA=", /* 52 */516314518, /* 53 */"KEJO3kdyW/ZPYnwucLJ2GpCO", /* 54 */"uk013cKknCv5n850", /* 55 */953091927, /* 56 */1146701784, /* 57 */363613807, /* 58 */"TnNS5U6tUj0=", /* 59 */"QHpZ5mkqI74=", /* 60 */-262003838, /* 61 */"4SzPpMfTN0ymxCZEm80XPA==", /* 62 */"CA2khRsFh44EG5CnISs=", /* 63 */-1866703220, /* 64 */"siIttn8yep5BAg==", /* 65 */-2088161104, /* 66 */-1955516945, /* 67 */-791733478, /* 68 */-1182434287, /* 69 */-98681663, /* 70 */"5tCweaLhuSjmsA==", /* 71 */1295585065, /* 72 */"d35PimECfb4=", /* 73 */"Ep+LKg+l6d3bcvj2xkM=", /* 74 */-280893284, /* 75 */"VJmmMRSpvCAYa4r3LVvfwFg5F1E=", /* 76 */502283496, /* 77 */"8VbQ+rBvAxuHtkUJ2YE=", /* 78 */"fy1JvYsUBIPu/w==", /* 79 */true, /* 80 */"rsc9Q+/c0rXtJfKc+Gw=", /* 81 */"P2Gqpmwzu4/bAs+Gw/GTmaQm", /* 82 */"SZB+CHaAsggtkJk5", /* 83 */"NvGgSdXG6To=", /* 84 */"cUhQwElI", /* 85 */679596965, /* 86 */-1470645520, /* 87 */1708669904, /* 88 */447157396, /* 89 */"SSBiuG3QSkhAwGZI", /* 90 */-2124101123, /* 91 */"B2K87RdWRjoAzgJavvYwaqbdK6Y=", /* 92 */1599205118, /* 93 */"yBbsk84/", /* 94 */"uBQIhZ0N+5zHNN2j", /* 95 */-1924372984, /* 96 */-52966278, /* 97 */-1093443340, /* 98 */"btpQtm4qOZ62ajL23Bk=", /* 99 */-141022586, /* 100 */"4sDmSTXwlmgfIIC/", /* 101 */814886513, /* 102 */"+blKDi2atXcm6vleBqo=", /* 103 */1517631829, /* 104 */"UNBoSEZAb9h4cAzorGF1qFow", /* 105 */1177535669, /* 106 */637689932, /* 107 */-685233468, /* 108 */"AsK0xjBSo+0rZqialS8=", /* 109 */289179573, /* 110 */"2nm05rGq7D8=", /* 111 */364370658, /* 112 */"Nk6q0TF6me43Gnm2", /* 113 */-235847848, /* 114 */"RmZTqkMyV0ZUwod/MtI=", /* 115 */-1242038883, /* 116 */1205308688, /* 117 */1247577077, /* 118 */"ADGqiQ8BYn8=", /* 119 */-1291123972, /* 120 */"En2rzBxXpCNPr20VZfUEDeyc", /* 121 */-898916897, /* 122 */"TVN4z4JAKdlNUTQ5Uqc=", /* 123 */-48820549, /* 124 */"4qjRIMGZXRCR+DYAuIs=", /* 125 */"DGK09iQS", /* 126 */"JzyprCDkvnsEDLWEKQTUjfMc2+Q=", /* 127 */"E9yITOC39tvFbOn03ZX1Dcm88xT1kxd/", /* 128 */"bv4OCpaWbjo=", /* 129 */1510703074, /* 130 */"8mfm6u5e2sn5wfBe", /* 131 */"f/9tQVnJTbG9KiPnqns=", /* 132 */-350854733, /* 133 */"", /* 134 */1531505986, /* 135 */"jswtRJnVLiznvCskhGv2/A==", /* 136 */"7Xnj0pUujqMqFqySIP6jgQ==", /* 137 */1979033973, /* 138 */-111157380, /* 139 */2111043151, /* 140 */-1807469845, /* 141 */"OUpu8itWSCJivkwS", /* 142 */1657945841, /* 143 */"wdLeV1/DvE4F1Q==", /* 144 */-1806386050, /* 145 */"nBHeepUO5ZuWN7eq", /* 146 */454846104, /* 147 */"YHlr0YtYFyPYrzUTmPg9AQ==", /* 148 */1971629099, /* 149 */35171822, /* 150 */275581739, /* 151 */"Tl6wyUEyRF5h4g==", /* 152 */-614169613, /* 153 */"7LfRChWBrgnE8twe+KM=", /* 154 */-1219060107, /* 155 */-106753738, /* 156 */-1043023368, /* 157 */"xBAFiJwBxZjRMO23", /* 158 */512646000, /* 159 */"9w7nlf810K4eW7z3I0u63g==", /* 160 */1681983511, /* 161 */"igNdiOLwk5mdIfq4hlDGy9N/xsg=", /* 162 */-702711550, /* 163 */251739580, /* 164 */2066435286, /* 165 */"U+IWrogxD5q0DhT6txc=", /* 166 */-471370987, /* 167 */"QbRfHEhk", /* 168 */-398061619, /* 169 */"2CPfp6ASCY3bBRCFjg4Lu6In", /* 170 */-454440740, /* 171 */422574181, /* 172 */"M/GLeADDpU8os9rY", /* 173 */1197698224, /* 174 */"F5q3BweK", /* 175 */-364635412, /* 176 */462163878, /* 177 */348165668, /* 178 */"8nbqCumd8wU=", /* 179 */497464830, /* 180 */"lOsZH6mEyAXXndHsmGTo4w==", /* 181 */"RRW6jgE7jpdoa3L9WGV11Q==", /* 182 */-748086903, /* 183 */"g0UlzqdSGCamo2Qe", /* 184 */1291331727, /* 185 */-9520640, /* 186 */"BgyWhC8Ule0=", /* 187 */"/WHiKv222Vvyz8J6+tH0qeIi66Y=", /* 188 */"lrNQ14hMEP0=", /* 189 */2688070, /* 190 */"h3RFz8tbAzc=", /* 191 */-1605030575, /* 192 */"qQowkbN5J+E=", /* 193 */191266769, /* 194 */-1860918319, /* 195 */-1377581996, /* 196 */1063960161, /* 197 */944981213, /* 198 */-218057703, /* 199 */1497849076, /* 200 */-1093909407, /* 201 */"HRBRiH8AbphPEBu4viAC6Gpwcsg=", /* 202 */-1184585905, /* 203 */1125595010, /* 204 */-1299534696, /* 205 */227940882, /* 206 */"JD9YQ2TFWX1u7WolT7tkLw==", /* 207 */"UcZhSkc+", /* 208 */-435650233, /* 209 */"Wat34Wp5", /* 210 */-1467005087, /* 211 */-2124865817, /* 212 */-1164826739]; +// Closure & Frame +function Closure(fake_11, fn, fake_12) { + this.R = fn; + this.ah = []; + this.D = fake_12; + this.u = fake_11; + this.prototype = {}; // <- default prototype object for `new` +} +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} +var BYTECODE = [82, 83, 84, 85, 47, 6, 3779, 1, 86, 0, 0, 144, 7, 4, 3257203871, 5, 6, 1, 7, 47, 8, 6730, 4, 101, 0, 0, 86, 47, 16, 117, 1, 103, 1, 0, 1, 5, 61, 2, 13, 47, 45366795, 13, 12, 5, 16, 13, 15, 87, 88, 89, 90, 42, 3, 8, 4, 5, 9, 10, 11, 145, 2039942731, 17, 7, 62, 18, 17, 12, 8, 12, 12, 13, 12, 2857175795, 13, 12, 42, 17, 2, 1, 18, 144, 18, 9, 1930097231, 19, 2, 1, 18, 91, 116, 9, 3556613868, 10, 212, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 92, 48, 18, 3, 17, 19, 20, 93, 145, 0, 16, 10, 94, 119, 98, 11, 3430131245, 101, 2, 672545599, 99, 12, 2898240806, 100, 13, 477188763, 47, 94, 10449, 2, 13, 0, 0, 95, 145, 3053235908, 20, 14, 61, 21, 99, 153, 2089930795, 99, 99, 15, 20, 99, 101, 145, 672545599, 22, 2, 61, 23, 99, 168, 1341999285, 98, 98, 16, 22, 99, 101, 145, 3284837567, 24, 17, 63, 99, 99, 67135993, 99, 99, 99, 21, 99, 25, 24, 99, 99, 18, 99, 98, 64, 25, 96, 99, 99, 98, 98, 99, 99, 99, 98, 99, 65, 98, 19, 96, 98, 98, 96, 3101106206, 98, 115, 92, 259944497, 97, 3685119927, 66, 99, 98, 97, 98, 99, 229, 92, 99, 98, 101, 97, 99, 99, 99, 67, 98, 20, 97, 96, 872108591, 99, 98, 97, 99, 68, 98, 99, 98, 92, 99, 99, 99, 97, 92, 98, 98, 146, 1718152023, 93, 77, 145, 832463161, 26, 21, 61, 23, 98, 270, 3356184819, 99, 98, 22, 26, 98, 101, 69, 98, 21, 99, 13662, 23, 99, 99, 99, 27, 64, 27, 96, 98, 98, 98, 98, 98, 98, 98, 99, 98, 65, 98, 15, 96, 99, 98, 96, 2089930795, 99, 62, 96, 96, 99, 23, 98, 99, 98, 99, 3190261604, 99, 99, 115, 92, 3526521455, 97, 3452428797, 66, 99, 98, 97, 99, 99, 333, 92, 99, 98, 101, 97, 98, 98, 98, 70, 97, 97, 98, 99, 99, 98, 99, 99, 99, 99, 96, 99, 68, 98, 99, 98, 92, 99, 99, 98, 97, 92, 98, 99, 146, 4107289942, 93, 77, 115, 92, 223296982, 93, 427197638, 77, 145, 3672365103, 28, 24, 68, 99, 98, 98, 23, 99, 98, 99, 28, 29, 99, 98, 61, 23, 98, 395, 2989996262, 98, 99, 25, 29, 99, 101, 69, 99, 21, 99, 41492, 23, 98, 99, 99, 30, 64, 30, 96, 99, 98, 98, 99, 98, 98, 98, 98, 98, 65, 99, 26, 96, 99, 98, 96, 1297108192, 98, 115, 92, 223635961, 97, 3191375423, 66, 98, 99, 97, 98, 99, 446, 92, 98, 99, 101, 97, 98, 99, 98, 67, 98, 27, 97, 97, 81358636, 99, 98, 96, 99, 68, 98, 98, 98, 92, 99, 99, 99, 97, 92, 99, 99, 146, 1935005068, 93, 77, 115, 92, 1706808689, 93, 351809382, 77, 145, 401208470, 31, 28, 66, 99, 99, 32, 99, 99, 497, 31, 99, 99, 101, 23, 99, 98, 99, 61, 23, 98, 508, 2860714620, 98, 99, 29, 32, 99, 101, 69, 98, 21, 99, 9125, 23, 99, 99, 99, 33, 64, 33, 96, 98, 98, 99, 98, 98, 99, 98, 99, 98, 65, 98, 30, 96, 98, 99, 96, 2249185396, 98, 62, 96, 96, 98, 31, 98, 98, 99, 99, 3892987275, 98, 99, 115, 92, 2831370448, 97, 2518086654, 66, 98, 99, 97, 98, 99, 571, 92, 98, 98, 101, 97, 98, 99, 99, 70, 97, 96, 98, 99, 99, 99, 98, 99, 99, 98, 97, 99, 68, 98, 98, 99, 92, 99, 99, 99, 97, 92, 98, 98, 146, 3816758929, 93, 77, 115, 92, 1982393997, 93, 3419385022, 77, 145, 3380613112, 34, 32, 68, 99, 99, 99, 23, 99, 99, 99, 34, 35, 99, 99, 61, 23, 98, 633, 2089930795, 98, 98, 15, 35, 99, 101, 69, 98, 21, 98, 42816, 23, 98, 98, 98, 36, 64, 36, 96, 99, 99, 99, 99, 99, 98, 98, 98, 99, 65, 99, 5, 96, 99, 98, 96, 45366795, 99, 62, 96, 96, 98, 33, 98, 99, 98, 98, 127993827, 98, 99, 115, 92, 302718611, 97, 3897025661, 66, 99, 99, 97, 98, 99, 696, 92, 99, 98, 101, 97, 98, 99, 99, 70, 97, 96, 99, 99, 98, 99, 98, 98, 99, 98, 97, 99, 68, 99, 99, 99, 92, 99, 99, 98, 97, 92, 98, 99, 146, 2038700104, 93, 77, 115, 92, 2593197062, 93, 1292543062, 77, 145, 829275968, 37, 34, 66, 98, 98, 38, 98, 98, 750, 37, 98, 98, 101, 23, 98, 99, 98, 61, 23, 98, 761, 3101106206, 98, 98, 19, 38, 99, 101, 69, 98, 21, 98, 24632, 23, 98, 99, 99, 39, 64, 39, 96, 98, 98, 98, 98, 98, 99, 99, 98, 99, 65, 99, 25, 96, 99, 98, 96, 2989996262, 98, 115, 92, 1410000576, 97, 3751172450, 66, 98, 98, 97, 99, 98, 812, 92, 98, 98, 101, 97, 99, 99, 99, 67, 98, 35, 97, 97, 933279698, 98, 98, 96, 98, 68, 98, 98, 98, 92, 98, 98, 98, 97, 92, 98, 98, 146, 1702809159, 93, 77, 115, 92, 3149009105, 93, 3755723989, 77, 145, 350643841, 40, 36, 68, 99, 99, 99, 23, 98, 99, 98, 40, 41, 98, 99, 61, 23, 99, 871, 2153582824, 98, 99, 37, 41, 98, 101, 69, 98, 21, 99, 18314, 23, 99, 99, 98, 42, 64, 42, 96, 99, 99, 99, 99, 99, 98, 99, 99, 99, 65, 98, 25, 96, 98, 98, 96, 2989996262, 98, 62, 96, 96, 98, 38, 98, 99, 99, 99, 3030165953, 99, 98, 115, 92, 1844255052, 97, 2157538846, 66, 99, 98, 97, 98, 99, 934, 92, 99, 98, 101, 97, 99, 98, 99, 70, 97, 97, 98, 99, 98, 98, 99, 98, 99, 98, 96, 99, 68, 99, 99, 99, 92, 98, 98, 98, 97, 92, 98, 98, 146, 153849103, 93, 77, 115, 92, 3830613391, 93, 2588396867, 77, 145, 461950025, 43, 39, 66, 98, 99, 44, 98, 99, 988, 43, 98, 98, 101, 23, 98, 98, 98, 61, 23, 98, 999, 2089930795, 99, 99, 15, 44, 98, 101, 69, 98, 21, 99, 64584, 23, 98, 98, 99, 45, 64, 45, 96, 98, 98, 99, 99, 99, 98, 98, 99, 99, 65, 99, 40, 96, 98, 99, 96, 3046477944, 99, 62, 96, 96, 99, 41, 98, 98, 99, 99, 2120941144, 98, 99, 115, 92, 3224704920, 97, 3537988714, 66, 99, 98, 97, 99, 98, 1062, 92, 99, 98, 101, 97, 99, 98, 99, 70, 97, 97, 98, 98, 99, 98, 98, 99, 99, 98, 96, 98, 68, 99, 98, 98, 92, 98, 98, 99, 97, 92, 98, 98, 146, 19245675, 93, 77, 115, 92, 2370297129, 93, 121865900, 77, 145, 3252273617, 46, 42, 68, 98, 98, 98, 23, 98, 98, 98, 46, 47, 98, 98, 61, 23, 98, 1124, 2089930795, 98, 98, 15, 47, 98, 101, 69, 98, 21, 98, 3942, 23, 98, 98, 98, 48, 64, 48, 96, 98, 99, 98, 98, 98, 98, 98, 98, 98, 65, 99, 43, 96, 98, 98, 96, 3403343297, 99, 115, 92, 731721863, 97, 2058949697, 66, 99, 98, 97, 99, 98, 1175, 92, 98, 98, 101, 97, 98, 98, 99, 67, 98, 44, 97, 96, 3100933761, 98, 99, 97, 99, 68, 98, 98, 99, 92, 99, 99, 98, 97, 92, 98, 99, 146, 697111280, 93, 77, 115, 92, 710536699, 93, 2282007961, 77, 145, 833852373, 49, 45, 68, 98, 98, 98, 23, 98, 98, 98, 49, 50, 98, 98, 61, 23, 98, 1234, 2153582824, 98, 98, 37, 50, 98, 101, 69, 99, 21, 98, 4677, 23, 98, 98, 98, 51, 64, 51, 96, 99, 98, 98, 98, 99, 98, 99, 98, 98, 65, 99, 26, 96, 98, 98, 96, 1297108192, 98, 115, 92, 2310399559, 97, 208418821, 66, 98, 98, 97, 99, 99, 1285, 92, 98, 98, 101, 97, 98, 98, 98, 67, 98, 46, 97, 97, 1919997471, 98, 98, 96, 98, 68, 99, 99, 99, 92, 98, 98, 98, 97, 92, 99, 98, 146, 1467056741, 93, 77, 115, 92, 1737911034, 93, 3904601737, 77, 145, 1936871732, 52, 47, 68, 99, 98, 99, 23, 98, 99, 99, 52, 53, 99, 99, 61, 23, 99, 1344, 2249185396, 99, 99, 30, 53, 99, 101, 69, 98, 21, 98, 41354, 23, 99, 98, 99, 54, 64, 54, 96, 99, 98, 98, 99, 98, 98, 99, 98, 99, 65, 99, 5, 96, 98, 98, 96, 45366795, 99, 62, 96, 96, 98, 48, 99, 99, 99, 98, 3009285883, 98, 98, 115, 92, 3548710158, 97, 2848049912, 66, 99, 98, 97, 98, 99, 1407, 92, 98, 99, 101, 97, 98, 98, 98, 70, 97, 97, 98, 98, 98, 99, 98, 98, 99, 98, 96, 98, 68, 99, 98, 99, 92, 98, 99, 99, 97, 92, 99, 98, 146, 1555328988, 93, 77, 115, 92, 198989265, 93, 996946702, 77, 145, 2168712028, 55, 49, 66, 98, 99, 56, 99, 98, 1461, 55, 99, 98, 101, 23, 98, 98, 98, 61, 23, 99, 1472, 3690386195, 98, 98, 50, 56, 98, 101, 69, 99, 21, 99, 6719, 23, 98, 99, 99, 57, 64, 57, 96, 99, 99, 98, 98, 99, 99, 99, 99, 99, 65, 98, 19, 96, 99, 99, 96, 3101106206, 98, 62, 96, 96, 99, 51, 99, 98, 98, 99, 2730944075, 99, 98, 115, 92, 3314136458, 97, 3388229116, 66, 98, 99, 97, 99, 99, 1535, 92, 99, 99, 101, 97, 99, 98, 98, 70, 97, 96, 98, 98, 99, 99, 98, 98, 98, 99, 97, 99, 68, 98, 98, 99, 92, 99, 99, 99, 97, 92, 99, 99, 146, 3458760546, 93, 77, 115, 92, 1631411994, 93, 492312266, 77, 145, 516310971, 58, 52, 66, 99, 98, 59, 98, 99, 1589, 58, 98, 99, 101, 23, 99, 99, 99, 61, 23, 98, 1600, 3403343297, 99, 99, 43, 59, 99, 101, 69, 99, 21, 99, 30813, 23, 98, 99, 98, 60, 64, 60, 96, 98, 98, 99, 99, 99, 99, 98, 99, 99, 65, 98, 53, 96, 99, 98, 96, 4181491721, 98, 115, 92, 471569984, 97, 3200118310, 66, 99, 99, 97, 99, 99, 1651, 92, 98, 98, 101, 97, 99, 98, 98, 67, 98, 54, 97, 97, 2484888979, 99, 98, 96, 99, 68, 99, 99, 98, 92, 99, 99, 99, 97, 92, 99, 98, 146, 1126536341, 93, 77, 115, 92, 3643028304, 93, 682157094, 77, 145, 953116319, 61, 55, 68, 98, 98, 99, 23, 98, 99, 98, 61, 62, 98, 99, 61, 23, 98, 1710, 45366795, 98, 99, 5, 62, 98, 101, 69, 99, 21, 99, 34392, 23, 98, 98, 99, 63, 64, 63, 96, 99, 98, 99, 98, 98, 98, 99, 98, 99, 65, 99, 37, 96, 99, 99, 96, 2153582824, 98, 115, 92, 2662458014, 97, 2952388636, 66, 99, 99, 97, 98, 98, 1761, 92, 99, 99, 101, 97, 99, 99, 98, 67, 99, 56, 97, 96, 1146708096, 98, 99, 97, 99, 68, 98, 98, 99, 92, 99, 99, 98, 97, 92, 98, 98, 146, 1596410853, 93, 77, 115, 92, 2749073479, 93, 2309127318, 77, 145, 363599444, 64, 57, 68, 98, 98, 98, 23, 99, 99, 99, 64, 65, 99, 98, 61, 23, 99, 1820, 45366795, 99, 99, 5, 65, 99, 101, 69, 98, 21, 99, 6453, 23, 98, 98, 99, 66, 64, 66, 96, 98, 98, 98, 98, 98, 99, 99, 98, 99, 65, 98, 58, 96, 98, 98, 96, 3494415691, 98, 62, 96, 96, 98, 59, 98, 99, 98, 98, 1273803114, 98, 98, 115, 92, 2944074052, 97, 1616846218, 66, 98, 98, 97, 98, 99, 1883, 92, 98, 98, 101, 97, 99, 98, 99, 70, 97, 96, 98, 99, 98, 98, 99, 99, 98, 99, 97, 98, 68, 99, 99, 98, 92, 98, 98, 98, 97, 92, 98, 99, 146, 300414335, 93, 77, 115, 92, 60740734, 93, 2033087995, 77, 145, 4032998442, 67, 60, 66, 98, 99, 68, 99, 98, 1937, 67, 98, 98, 101, 23, 98, 98, 98, 61, 23, 98, 1948, 1341999285, 98, 99, 16, 68, 99, 101, 69, 99, 21, 99, 11266, 23, 98, 99, 98, 69, 64, 69, 96, 99, 99, 98, 98, 99, 99, 98, 98, 99, 65, 98, 61, 96, 98, 99, 96, 1960262425, 98, 115, 92, 2741086911, 97, 4068314745, 66, 98, 99, 97, 99, 98, 1999, 92, 98, 99, 101, 97, 98, 98, 98, 67, 98, 62, 97, 96, 1432164406, 99, 98, 97, 99, 68, 98, 98, 98, 92, 98, 98, 98, 97, 92, 98, 99, 146, 4145096490, 93, 77, 115, 92, 2705329728, 93, 1306703015, 77, 145, 2428260318, 70, 63, 68, 99, 98, 98, 23, 99, 99, 99, 70, 71, 99, 98, 61, 23, 98, 2058, 2153582824, 98, 99, 37, 71, 98, 101, 69, 99, 21, 99, 11815, 23, 99, 98, 99, 72, 64, 72, 96, 98, 98, 98, 99, 99, 98, 99, 98, 99, 65, 99, 19, 96, 99, 98, 96, 3101106206, 98, 62, 96, 96, 99, 64, 99, 98, 98, 99, 4185204167, 98, 99, 115, 92, 279112108, 97, 592395902, 66, 99, 99, 97, 99, 98, 2121, 92, 98, 98, 101, 97, 99, 98, 98, 70, 97, 96, 99, 99, 99, 99, 99, 99, 98, 99, 97, 99, 68, 98, 98, 98, 92, 98, 99, 99, 97, 92, 98, 98, 146, 586809893, 93, 77, 115, 92, 1805631167, 93, 3861516091, 77, 145, 2206811938, 73, 65, 68, 98, 99, 99, 23, 99, 99, 98, 73, 74, 98, 99, 61, 23, 99, 2183, 3046477944, 99, 99, 40, 74, 98, 101, 69, 99, 21, 98, 8715, 23, 99, 98, 99, 75, 64, 75, 96, 99, 98, 99, 98, 99, 98, 98, 98, 99, 65, 99, 26, 96, 99, 99, 96, 1297108192, 98, 115, 92, 843392647, 97, 3184564521, 66, 99, 98, 97, 98, 99, 2234, 92, 99, 99, 101, 97, 99, 99, 99, 67, 99, 66, 97, 97, 2339482580, 98, 98, 96, 98, 68, 99, 99, 98, 92, 98, 98, 98, 97, 92, 98, 99, 146, 1879420403, 93, 77, 115, 92, 3700652143, 93, 2902232259, 77, 145, 3503289817, 76, 67, 66, 99, 99, 77, 99, 99, 2285, 76, 99, 99, 101, 23, 99, 99, 99, 61, 23, 98, 2296, 2989996262, 99, 98, 25, 77, 99, 101, 69, 99, 21, 98, 62396, 23, 99, 98, 98, 78, 64, 78, 96, 98, 98, 98, 99, 98, 99, 99, 98, 98, 65, 98, 26, 96, 98, 98, 96, 1297108192, 99, 115, 92, 2781270229, 97, 1454042395, 66, 99, 99, 97, 99, 98, 2347, 92, 98, 98, 101, 97, 99, 99, 99, 67, 99, 68, 97, 96, 3112509210, 99, 99, 97, 99, 68, 99, 99, 99, 92, 99, 99, 99, 97, 92, 98, 99, 146, 3753395510, 93, 77, 115, 92, 2039578735, 93, 1600837485, 77, 145, 4196270275, 79, 69, 68, 98, 98, 98, 23, 98, 98, 98, 79, 80, 98, 98, 61, 23, 99, 2406, 45366795, 99, 98, 5, 80, 99, 101, 69, 99, 21, 99, 52936, 23, 98, 98, 98, 81, 64, 81, 96, 98, 99, 99, 99, 98, 98, 99, 99, 99, 65, 99, 43, 96, 98, 98, 96, 3403343297, 98, 62, 96, 96, 99, 70, 99, 98, 99, 98, 1575991295, 98, 98, 115, 92, 4095256375, 97, 3781972581, 66, 98, 98, 97, 98, 98, 2469, 92, 98, 98, 101, 97, 99, 99, 99, 70, 97, 96, 98, 98, 98, 99, 99, 98, 98, 98, 97, 99, 68, 99, 99, 98, 92, 99, 98, 99, 97, 92, 99, 99, 146, 2793422639, 93, 77, 115, 92, 514203639, 93, 1324191322, 77, 145, 1295600769, 82, 71, 68, 98, 98, 99, 23, 99, 99, 98, 82, 83, 98, 99, 61, 23, 99, 2531, 982859421, 98, 99, 72, 83, 99, 101, 69, 98, 21, 99, 54922, 23, 99, 99, 99, 84, 64, 84, 96, 99, 99, 99, 99, 98, 99, 99, 98, 99, 65, 99, 19, 96, 99, 98, 96, 3101106206, 99, 62, 96, 96, 99, 73, 99, 98, 98, 98, 384416945, 99, 98, 115, 92, 2573199160, 97, 141021466, 66, 99, 98, 97, 98, 98, 2594, 92, 99, 99, 101, 97, 99, 99, 98, 70, 97, 97, 99, 98, 99, 98, 99, 99, 99, 98, 96, 99, 68, 99, 98, 99, 92, 98, 99, 99, 97, 92, 98, 98, 146, 2097024512, 93, 77, 115, 92, 1385426066, 93, 2517364142, 77, 145, 4014062813, 85, 74, 66, 99, 99, 86, 99, 99, 2648, 85, 99, 99, 101, 23, 99, 99, 99, 61, 23, 99, 2659, 3494415691, 99, 98, 58, 86, 98, 101, 69, 98, 21, 99, 17363, 23, 99, 99, 99, 87, 64, 87, 96, 98, 99, 99, 99, 98, 99, 98, 99, 98, 65, 98, 25, 96, 98, 98, 96, 2989996262, 99, 62, 96, 96, 98, 75, 98, 99, 99, 99, 2537601339, 99, 99, 115, 92, 2403065046, 97, 2328972388, 66, 99, 99, 97, 99, 99, 2722, 92, 99, 98, 101, 97, 98, 99, 98, 70, 97, 97, 99, 99, 99, 99, 99, 98, 98, 99, 96, 99, 68, 99, 99, 99, 92, 98, 98, 99, 97, 92, 98, 98, 146, 2375012371, 93, 77, 115, 92, 1141270692, 93, 565607943, 77, 145, 502311360, 88, 76, 68, 98, 99, 98, 23, 99, 98, 99, 88, 89, 99, 99, 61, 23, 99, 2784, 2249185396, 99, 98, 30, 89, 99, 101, 69, 98, 21, 99, 61667, 23, 98, 99, 99, 90, 64, 90, 96, 99, 99, 99, 99, 98, 99, 98, 99, 99, 65, 99, 15, 96, 98, 99, 96, 2089930795, 98, 115, 92, 3758057470, 97, 3057397224, 66, 99, 98, 97, 99, 99, 2835, 92, 99, 99, 101, 97, 99, 99, 99, 67, 98, 77, 97, 96, 1984134043, 98, 98, 97, 99, 68, 98, 98, 99, 92, 99, 98, 99, 97, 92, 98, 98, 146, 3931249606, 93, 77, 115, 92, 3117066657, 93, 4113859348, 77, 115, 92, 361444591, 93, 1475631768, 77, 146, 21269, 21, 80, 96, 116, 16, 2605203216, 17, 225, 91, 12004, 66, 99, 99, 21, 99, 99, 2900, 91, 99, 99, 101, 21, 99, 99, 99, 115, 92, 3350775122, 93, 1642221825, 77, 97, 47, 15, 6730, 4, 101, 0, 0, 98, 146, 16720, 91, 66, 98, 99, 21, 98, 99, 2933, 91, 98, 98, 101, 21, 99, 99, 98, 115, 92, 399841737, 93, 1791630986, 77, 61, 5, 98, 2950, 3101106206, 98, 99, 19, 9, 98, 101, 146, 3716, 4, 55, 146, 59119, 91, 68, 98, 99, 98, 21, 99, 99, 99, 91, 21, 99, 99, 115, 92, 1286850687, 93, 2904623171, 77, 146, 63354, 21, 99, 116, 16, 3556613868, 17, 212, 91, 1732, 68, 99, 99, 99, 21, 98, 98, 99, 91, 21, 99, 98, 115, 92, 3180899883, 93, 2515675148, 77, 100, 101, 120, 6, 2, 672545599, 91, 9824, 68, 99, 98, 99, 21, 99, 98, 98, 91, 21, 99, 99, 115, 92, 3526434431, 93, 1023195140, 77, 145, 2039942731, 102, 7, 68, 98, 98, 98, 0, 98, 98, 98, 102, 9, 98, 99, 55, 146, 13358, 91, 66, 98, 98, 21, 98, 98, 3065, 91, 98, 98, 101, 21, 98, 99, 99, 115, 92, 3633228349, 93, 1771781051, 77, 146, 2873, 4, 55, 146, 61123, 91, 68, 99, 99, 99, 21, 99, 99, 99, 91, 21, 99, 99, 115, 92, 415255207, 93, 1459644403, 77, 102, 103, 146, 21569, 91, 66, 99, 99, 21, 99, 98, 3116, 91, 99, 99, 101, 21, 99, 98, 98, 115, 92, 2560524215, 93, 596982225, 77, 146, 43292, 91, 71, 98, 99, 99, 99, 91, 99, 21, 99, 98, 91, 98, 99, 64, 91, 96, 99, 99, 98, 99, 99, 99, 99, 99, 99, 65, 99, 40, 96, 98, 99, 96, 3046477944, 98, 62, 96, 96, 98, 78, 99, 98, 99, 98, 2292228560, 98, 99, 115, 92, 1561712963, 97, 2575657003, 66, 98, 98, 97, 98, 98, 3191, 92, 99, 98, 101, 97, 98, 98, 98, 70, 97, 97, 98, 98, 98, 98, 98, 99, 98, 98, 96, 98, 68, 98, 98, 99, 92, 99, 99, 98, 97, 92, 98, 98, 146, 3085771448, 93, 77, 146, 43891, 91, 66, 99, 99, 21, 99, 98, 3238, 91, 99, 98, 101, 21, 98, 98, 99, 115, 92, 681795809, 93, 981683651, 77, 146, 43765, 91, 66, 98, 98, 21, 98, 99, 3262, 91, 98, 99, 101, 21, 99, 99, 99, 115, 92, 1420033890, 93, 2079189977, 77, 55, 146, 46831, 91, 66, 99, 99, 21, 99, 99, 3287, 91, 99, 99, 101, 21, 99, 99, 99, 115, 92, 2290465070, 93, 3537657777, 77, 81, 42, 12, 15, 4, 14, 16, 17, 18, 120, 13, 79, 0, 91, 30173, 66, 99, 98, 21, 98, 98, 3323, 91, 99, 99, 101, 21, 98, 98, 99, 115, 92, 2103959343, 93, 295372993, 77, 146, 51528, 21, 145, 0, 102, 10, 104, 146, 8776, 91, 66, 98, 99, 21, 98, 99, 3355, 91, 99, 98, 101, 21, 99, 99, 99, 115, 92, 888014446, 93, 2082442058, 77, 146, 13344, 21, 105, 146, 43778, 91, 71, 99, 99, 99, 99, 91, 98, 21, 99, 98, 91, 99, 99, 64, 91, 96, 98, 98, 98, 98, 98, 99, 99, 98, 99, 65, 99, 22, 96, 99, 98, 96, 3356184819, 99, 62, 96, 96, 99, 80, 98, 99, 99, 98, 3868289024, 98, 98, 115, 92, 651389404, 97, 3656175195, 66, 98, 99, 97, 99, 99, 3434, 92, 99, 99, 101, 97, 99, 99, 99, 70, 97, 97, 99, 99, 99, 99, 98, 99, 99, 99, 96, 98, 68, 99, 98, 99, 92, 98, 99, 99, 97, 92, 98, 98, 146, 2548227867, 93, 77, 146, 16060, 91, 68, 98, 99, 99, 21, 99, 99, 98, 91, 21, 98, 99, 115, 92, 121123247, 93, 1612865159, 77, 146, 555, 91, 66, 98, 98, 21, 99, 99, 3502, 91, 98, 98, 101, 21, 99, 99, 99, 115, 92, 1504173549, 93, 2446916074, 77, 71, 98, 98, 99, 99, 102, 99, 0, 99, 98, 6, 99, 99, 64, 102, 96, 98, 99, 98, 99, 99, 98, 99, 99, 99, 65, 99, 61, 96, 98, 98, 96, 1960262425, 98, 62, 96, 96, 98, 81, 98, 98, 98, 98, 448080990, 98, 98, 115, 92, 2395385859, 97, 3409329899, 66, 99, 99, 97, 98, 99, 3574, 92, 98, 98, 101, 97, 99, 98, 98, 70, 97, 96, 98, 98, 99, 99, 99, 98, 98, 98, 97, 98, 68, 99, 98, 98, 92, 99, 99, 99, 97, 92, 99, 99, 146, 3248786982, 93, 77, 146, 24975, 91, 66, 99, 98, 21, 99, 99, 3621, 91, 98, 98, 101, 21, 98, 98, 98, 115, 92, 2887000800, 93, 2755171340, 77, 146, 15473, 91, 68, 98, 98, 99, 21, 98, 98, 99, 91, 21, 98, 99, 115, 92, 1346796006, 93, 2182689514, 77, 55, 115, 4, 3329, 91, 45887, 68, 99, 99, 99, 21, 98, 98, 98, 91, 21, 98, 99, 115, 92, 1987053237, 93, 291794688, 77, 42, 10, 15, 4, 14, 16, 17, 18, 106, 61, 5, 98, 3692, 1960262425, 98, 99, 61, 102, 98, 101, 146, 24300, 91, 66, 99, 99, 21, 99, 98, 3710, 91, 98, 99, 101, 21, 99, 99, 98, 115, 92, 1299437918, 93, 2656239731, 77, 146, 28310, 21, 80, 81, 42, 8, 15, 4, 14, 16, 17, 18, 44, 102, 7, 1, 8, 107, 139, 21, 42499, 16, 1928842937, 17, 238, 18, 7, 146, 15968, 91, 66, 99, 98, 21, 99, 99, 3762, 91, 98, 99, 101, 21, 99, 98, 99, 115, 92, 2884575410, 93, 1842304017, 77, 42, 92, 94, 2, 93, 92, 146, 0, 95, 108, 109, 128, 79, 82, 2236748468, 83, 2, 672545599, 81, 83, 4278623955, 80, 84, 3250273750, 82, 85, 679593628, 47, 76, 10744, 3, 16, 0, 0, 110, 145, 2824279855, 16, 86, 61, 17, 80, 3818, 2089930795, 81, 81, 15, 16, 80, 83, 145, 672545599, 18, 2, 61, 19, 79, 3833, 3494415691, 80, 81, 58, 18, 79, 83, 145, 1708681701, 20, 87, 63, 80, 80, 447153367, 80, 80, 79, 17, 79, 21, 20, 79, 80, 88, 80, 80, 64, 21, 77, 79, 79, 80, 80, 79, 79, 80, 79, 79, 65, 79, 15, 77, 80, 80, 77, 2089930795, 81, 62, 77, 77, 79, 89, 80, 79, 80, 81, 618381054, 81, 79, 115, 73, 3783263970, 78, 3753671865, 66, 81, 81, 78, 80, 79, 3906, 73, 81, 81, 83, 78, 81, 81, 81, 70, 78, 78, 81, 80, 81, 80, 81, 79, 81, 81, 77, 79, 68, 79, 80, 80, 73, 79, 79, 79, 78, 73, 81, 79, 115, 74, 14889, 75, 20256, 79, 145, 2170852026, 22, 90, 61, 19, 80, 3952, 2989996262, 79, 79, 25, 22, 80, 83, 69, 79, 17, 81, 63598, 19, 79, 80, 80, 23, 64, 23, 77, 80, 79, 81, 80, 79, 80, 81, 79, 81, 65, 81, 30, 77, 81, 79, 77, 2249185396, 80, 62, 77, 77, 80, 91, 79, 80, 80, 80, 3759281774, 79, 79, 115, 73, 935819762, 78, 3333232392, 66, 79, 80, 78, 79, 80, 4015, 73, 80, 80, 83, 78, 80, 81, 79, 70, 78, 78, 79, 79, 81, 81, 79, 79, 79, 80, 77, 81, 68, 79, 80, 80, 73, 81, 80, 80, 78, 73, 81, 79, 115, 74, 17281, 75, 46033, 79, 116, 73, 2567726809, 74, 49917, 75, 1454, 79, 145, 1599188120, 24, 92, 66, 80, 80, 25, 80, 81, 4073, 24, 80, 81, 83, 19, 81, 81, 81, 61, 19, 80, 4084, 1202307030, 80, 79, 93, 25, 79, 83, 69, 80, 17, 80, 25897, 19, 79, 80, 79, 26, 64, 26, 77, 81, 80, 80, 80, 81, 80, 80, 81, 80, 65, 81, 61, 77, 81, 80, 77, 1960262425, 79, 115, 73, 2868658836, 78, 971104174, 66, 79, 81, 78, 79, 81, 4135, 73, 80, 79, 83, 78, 79, 79, 79, 67, 81, 94, 78, 77, 1143705111, 81, 81, 78, 81, 68, 80, 81, 79, 73, 81, 81, 81, 78, 73, 80, 80, 115, 74, 37981, 75, 35450, 79, 116, 73, 2587775905, 74, 22266, 75, 45588, 79, 145, 2370610128, 27, 95, 68, 80, 81, 80, 19, 81, 81, 79, 27, 28, 81, 80, 61, 19, 79, 4198, 3403343297, 81, 80, 43, 28, 80, 83, 69, 81, 17, 81, 7041, 19, 80, 79, 81, 29, 64, 29, 77, 80, 81, 80, 79, 79, 79, 80, 79, 79, 65, 79, 40, 77, 79, 79, 77, 3046477944, 79, 115, 73, 3510517768, 78, 1612963106, 66, 81, 79, 78, 79, 79, 4249, 73, 79, 79, 83, 78, 79, 79, 79, 67, 80, 96, 78, 78, 4241968606, 79, 81, 77, 80, 68, 81, 80, 81, 73, 80, 79, 80, 78, 73, 79, 79, 115, 74, 38270, 75, 39040, 79, 116, 73, 2139471415, 74, 8394, 75, 11329, 79, 145, 3201528643, 30, 97, 68, 80, 79, 79, 19, 79, 79, 80, 30, 31, 79, 79, 61, 19, 80, 4312, 3046477944, 80, 81, 40, 31, 79, 83, 69, 79, 17, 80, 56701, 19, 79, 79, 79, 32, 64, 32, 77, 80, 79, 80, 81, 81, 80, 80, 81, 79, 65, 79, 37, 77, 81, 79, 77, 2153582824, 81, 62, 77, 77, 80, 98, 80, 81, 79, 79, 2466044374, 81, 81, 115, 73, 3243445982, 78, 1345891316, 66, 81, 80, 78, 81, 81, 4375, 73, 79, 79, 83, 78, 79, 79, 80, 70, 78, 77, 79, 79, 79, 79, 81, 81, 79, 81, 78, 79, 68, 79, 81, 81, 73, 79, 79, 80, 78, 73, 80, 80, 115, 74, 19405, 75, 18594, 79, 116, 73, 1421204553, 74, 20431, 75, 2832, 79, 145, 4153995383, 33, 99, 66, 79, 80, 34, 80, 81, 4433, 33, 80, 80, 83, 19, 81, 81, 81, 61, 19, 80, 4444, 3690386195, 80, 79, 50, 34, 80, 83, 69, 80, 17, 80, 7067, 19, 80, 79, 80, 35, 64, 35, 77, 80, 80, 80, 79, 81, 79, 80, 80, 79, 65, 79, 58, 77, 81, 80, 77, 3494415691, 81, 62, 77, 77, 80, 100, 80, 81, 81, 81, 3627022804, 81, 79, 115, 73, 1203599650, 78, 3601012284, 66, 79, 80, 78, 79, 81, 4507, 73, 79, 81, 83, 78, 79, 79, 80, 70, 78, 77, 80, 80, 81, 80, 80, 81, 81, 81, 78, 81, 68, 79, 80, 81, 73, 79, 81, 81, 78, 73, 81, 80, 115, 74, 33172, 75, 30724, 79, 116, 73, 618435956, 74, 49380, 75, 59751, 79, 145, 814895510, 36, 101, 68, 79, 80, 81, 19, 80, 80, 79, 36, 37, 79, 80, 61, 19, 80, 4573, 1297108192, 81, 81, 26, 37, 81, 83, 69, 79, 17, 81, 12444, 19, 80, 81, 81, 38, 64, 38, 77, 79, 80, 81, 79, 79, 79, 80, 80, 79, 65, 81, 22, 77, 81, 80, 77, 3356184819, 81, 115, 73, 3473090454, 78, 1575535792, 66, 79, 79, 78, 80, 81, 4624, 73, 81, 79, 83, 78, 81, 80, 81, 67, 81, 102, 78, 78, 4270384657, 79, 81, 77, 81, 68, 80, 80, 80, 73, 79, 81, 80, 78, 73, 79, 80, 115, 74, 53687, 75, 9478, 79, 116, 73, 47893672, 74, 46396, 75, 63016, 79, 145, 1517640609, 39, 103, 68, 81, 80, 81, 19, 79, 79, 81, 39, 40, 80, 79, 61, 19, 79, 4687, 3403343297, 81, 79, 43, 40, 81, 83, 69, 81, 17, 80, 61963, 19, 81, 81, 80, 41, 64, 41, 77, 81, 80, 80, 80, 80, 80, 79, 80, 79, 65, 79, 93, 77, 79, 81, 77, 1202307030, 80, 62, 77, 77, 80, 104, 79, 81, 79, 81, 4103851241, 79, 80, 115, 73, 2607470040, 78, 709915377, 66, 80, 79, 78, 81, 80, 4750, 73, 79, 81, 83, 78, 80, 79, 81, 70, 78, 77, 79, 79, 80, 80, 79, 79, 80, 80, 78, 79, 68, 80, 81, 79, 73, 79, 80, 79, 78, 73, 79, 81, 115, 74, 43158, 75, 63701, 79, 116, 73, 2485364321, 74, 46709, 75, 40531, 79, 145, 1177539232, 42, 105, 66, 80, 81, 43, 80, 79, 4808, 42, 79, 80, 83, 19, 79, 79, 81, 61, 19, 80, 4819, 2860714620, 79, 79, 29, 43, 81, 83, 69, 79, 17, 79, 46732, 19, 81, 81, 80, 44, 64, 44, 77, 81, 81, 81, 80, 80, 81, 81, 81, 80, 65, 79, 58, 77, 80, 80, 77, 3494415691, 81, 115, 73, 2755888799, 78, 858334133, 66, 81, 81, 78, 81, 80, 4870, 73, 81, 81, 83, 78, 81, 81, 81, 67, 79, 106, 78, 78, 637665682, 80, 81, 77, 79, 68, 79, 79, 81, 73, 80, 81, 81, 78, 73, 79, 81, 115, 74, 4432, 75, 14811, 79, 116, 73, 2271233963, 74, 32250, 75, 27135, 79, 145, 3609762234, 45, 107, 66, 79, 81, 46, 79, 80, 4925, 45, 81, 79, 83, 19, 80, 80, 79, 61, 19, 81, 4936, 3046477944, 81, 81, 40, 46, 79, 83, 69, 79, 17, 80, 36776, 19, 81, 81, 79, 47, 64, 47, 77, 80, 80, 81, 80, 79, 81, 79, 80, 81, 65, 80, 43, 77, 79, 79, 77, 3403343297, 79, 62, 77, 77, 81, 108, 79, 80, 79, 79, 1386662634, 80, 80, 115, 73, 2702109907, 78, 804555246, 66, 79, 80, 78, 79, 79, 4999, 73, 80, 79, 83, 78, 80, 80, 79, 70, 78, 77, 79, 79, 80, 79, 79, 79, 79, 80, 78, 81, 68, 79, 81, 81, 73, 81, 80, 79, 78, 73, 81, 79, 115, 74, 19238, 75, 62172, 79, 116, 73, 2771453209, 74, 51497, 75, 41461, 79, 145, 289200068, 48, 109, 68, 80, 80, 80, 19, 80, 81, 80, 48, 49, 80, 80, 61, 19, 81, 5065, 2860714620, 80, 81, 29, 49, 80, 83, 69, 80, 17, 79, 14090, 19, 80, 79, 79, 50, 64, 50, 77, 80, 79, 80, 79, 79, 79, 79, 79, 81, 65, 81, 16, 77, 79, 79, 77, 1341999285, 80, 115, 73, 1263238407, 78, 3660651037, 66, 81, 79, 78, 79, 79, 5116, 73, 79, 80, 83, 78, 79, 79, 79, 67, 79, 110, 78, 77, 2484040196, 79, 79, 78, 81, 68, 81, 80, 80, 73, 80, 79, 80, 78, 73, 79, 80, 115, 74, 47501, 75, 31975, 79, 116, 73, 318392203, 74, 42099, 75, 57650, 79, 145, 364373607, 51, 111, 68, 80, 80, 80, 19, 80, 80, 79, 51, 52, 79, 80, 61, 19, 81, 5179, 1297108192, 79, 79, 26, 52, 81, 83, 69, 81, 17, 80, 52637, 19, 80, 80, 80, 53, 64, 53, 77, 79, 79, 79, 79, 81, 80, 80, 79, 80, 65, 79, 15, 77, 80, 80, 77, 2089930795, 79, 62, 77, 77, 79, 112, 81, 81, 79, 79, 2417148534, 81, 81, 115, 73, 3260670745, 78, 1363116083, 66, 80, 80, 78, 80, 81, 5242, 73, 79, 79, 83, 78, 80, 79, 81, 70, 78, 78, 81, 80, 80, 81, 80, 80, 81, 81, 77, 81, 68, 79, 81, 80, 73, 80, 81, 79, 78, 73, 81, 81, 115, 74, 55449, 75, 42511, 79, 116, 73, 459031548, 74, 49207, 75, 58989, 79, 145, 4059106243, 54, 113, 68, 81, 81, 81, 19, 80, 80, 81, 54, 55, 80, 80, 61, 19, 80, 5308, 2153582824, 81, 80, 37, 55, 81, 83, 69, 80, 17, 80, 2871, 19, 80, 81, 80, 56, 64, 56, 77, 80, 80, 80, 79, 80, 80, 81, 80, 80, 65, 80, 40, 77, 80, 80, 77, 3046477944, 81, 62, 77, 77, 81, 114, 80, 80, 80, 81, 3860187807, 80, 80, 115, 73, 1860134007, 78, 4257546637, 66, 80, 80, 78, 80, 81, 5371, 73, 80, 80, 83, 78, 80, 80, 80, 70, 78, 78, 80, 80, 80, 79, 80, 80, 80, 80, 77, 80, 68, 81, 79, 81, 73, 80, 80, 80, 78, 73, 80, 81, 115, 74, 56416, 75, 39360, 79, 116, 73, 340643307, 74, 59639, 75, 6798, 79, 145, 3052886237, 57, 115, 66, 79, 80, 58, 79, 79, 5429, 57, 81, 80, 83, 19, 79, 80, 79, 61, 19, 80, 5440, 2249185396, 79, 81, 30, 58, 80, 83, 69, 79, 17, 81, 41169, 19, 80, 79, 79, 59, 64, 59, 77, 81, 81, 81, 81, 80, 79, 81, 81, 81, 65, 79, 53, 77, 79, 81, 77, 4181491721, 81, 115, 73, 1801149067, 78, 4198561700, 66, 81, 80, 78, 79, 81, 5491, 73, 81, 81, 83, 78, 81, 81, 81, 67, 80, 116, 78, 77, 1205280130, 79, 81, 78, 79, 68, 80, 79, 79, 73, 80, 80, 80, 78, 73, 80, 80, 115, 74, 65482, 75, 59170, 79, 116, 73, 4154782578, 74, 17127, 75, 43150, 79, 145, 1247599220, 60, 117, 68, 79, 79, 79, 19, 81, 81, 81, 60, 61, 81, 81, 61, 19, 81, 5554, 3690386195, 81, 80, 50, 61, 79, 83, 69, 80, 17, 79, 13168, 19, 79, 81, 79, 62, 64, 62, 77, 79, 81, 79, 80, 80, 80, 81, 81, 80, 65, 80, 50, 77, 81, 81, 77, 3690386195, 81, 115, 73, 2460178892, 78, 562624230, 66, 80, 79, 78, 81, 79, 5605, 73, 81, 80, 83, 78, 80, 80, 80, 67, 81, 118, 78, 77, 2797706483, 80, 80, 78, 80, 68, 79, 81, 80, 73, 80, 79, 80, 78, 73, 81, 79, 115, 74, 65310, 75, 62498, 79, 116, 73, 1130198487, 74, 48497, 75, 42615, 79, 145, 3003844026, 63, 119, 68, 80, 80, 80, 19, 79, 79, 79, 63, 64, 79, 80, 61, 19, 81, 5668, 1202307030, 81, 80, 93, 64, 79, 83, 69, 81, 17, 81, 54325, 19, 81, 80, 81, 65, 64, 65, 77, 80, 80, 81, 80, 79, 81, 79, 81, 80, 65, 79, 50, 77, 79, 81, 77, 3690386195, 81, 62, 77, 77, 80, 120, 79, 79, 81, 79, 361916792, 79, 79, 115, 73, 1013570145, 78, 3410982775, 66, 81, 80, 78, 79, 79, 5731, 73, 80, 81, 83, 78, 79, 79, 79, 70, 78, 77, 80, 81, 81, 81, 79, 81, 81, 79, 78, 81, 68, 80, 79, 79, 73, 81, 81, 81, 78, 73, 79, 80, 115, 74, 943, 75, 60051, 79, 116, 73, 346875293, 74, 43721, 75, 34795, 79, 145, 3396013468, 66, 121, 66, 79, 80, 67, 80, 80, 5789, 66, 79, 79, 83, 19, 80, 80, 80, 61, 19, 79, 5800, 1341999285, 81, 81, 16, 67, 79, 83, 69, 81, 17, 80, 53170, 19, 80, 80, 81, 68, 64, 68, 77, 80, 80, 79, 79, 80, 79, 81, 80, 80, 65, 79, 43, 77, 81, 79, 77, 3403343297, 81, 62, 77, 77, 81, 122, 81, 81, 81, 79, 1494839264, 81, 80, 115, 73, 2657502823, 78, 759948158, 66, 81, 81, 78, 81, 80, 5863, 73, 79, 81, 83, 78, 79, 79, 80, 70, 78, 77, 79, 81, 79, 81, 81, 80, 81, 81, 78, 79, 68, 80, 81, 81, 73, 79, 80, 79, 78, 73, 81, 80, 115, 74, 14259, 75, 15971, 79, 116, 73, 1526984736, 74, 30792, 75, 21537, 79, 145, 4246177685, 69, 123, 68, 79, 80, 80, 19, 81, 79, 79, 69, 70, 79, 81, 61, 19, 79, 5929, 3356184819, 79, 79, 22, 70, 79, 83, 69, 80, 17, 79, 8937, 19, 81, 79, 79, 71, 64, 71, 77, 79, 80, 81, 81, 79, 81, 81, 79, 79, 65, 79, 40, 77, 80, 79, 77, 3046477944, 81, 115, 73, 898470778, 78, 3295883413, 66, 80, 79, 78, 79, 79, 5980, 73, 79, 79, 83, 78, 79, 79, 80, 67, 80, 124, 78, 78, 2705447340, 79, 79, 77, 79, 68, 80, 81, 81, 73, 80, 80, 79, 78, 73, 79, 79, 115, 74, 59703, 75, 56195, 79, 116, 73, 3528745164, 74, 2881, 75, 27028, 79, 116, 73, 4070153099, 74, 43849, 75, 64605, 79, 72, 83, 79, 80, 79, 6039, 81, 14, 12, 79, 79, 81, 81, 84, 81, 73, 2860714620, 84, 30, 4233837857, 79, 125, 2249185396, 80, 10, 85, 81, 81, 79, 29, 80, 43, 9, 7, 8, 1, 85, 146, 28960, 72, 66, 81, 80, 17, 81, 79, 6079, 72, 81, 79, 83, 17, 80, 81, 80, 116, 73, 3524138592, 74, 12404, 75, 7401, 79, 68, 79, 80, 79, 4, 81, 80, 81, 9, 84, 81, 79, 61, 4, 81, 6110, 2249185396, 79, 79, 30, 84, 79, 83, 146, 28984, 72, 68, 79, 81, 81, 17, 80, 79, 79, 72, 17, 81, 79, 116, 73, 1889050183, 74, 43149, 75, 32350, 79, 145, 145068275, 84, 126, 111, 43, 10, 3, 85, 1, 5, 146, 22913, 72, 66, 81, 81, 17, 80, 81, 6162, 72, 81, 81, 83, 17, 81, 81, 81, 116, 73, 1541123121, 74, 39587, 75, 37659, 79, 146, 10880, 72, 66, 81, 79, 17, 81, 80, 6188, 72, 79, 80, 83, 17, 80, 80, 81, 116, 73, 1534561773, 74, 47335, 75, 37759, 79, 125, 7, 5, 45366795, 84, 127, 732436688, 8, 7, 84, 146, 35069, 72, 68, 80, 79, 80, 17, 81, 79, 79, 72, 17, 79, 81, 116, 73, 949264032, 74, 43060, 75, 46690, 79, 149, 2, 148, 84, 434685269, 128, 42, 6, 84, 1, 0, 146, 4062, 72, 68, 80, 79, 80, 17, 80, 79, 79, 72, 17, 79, 80, 116, 73, 4242859814, 74, 24494, 75, 61970, 79, 146, 3940, 72, 66, 81, 79, 17, 79, 81, 6281, 72, 81, 81, 83, 17, 81, 80, 81, 116, 73, 1863991747, 74, 1297, 75, 32733, 79, 146, 57721, 72, 68, 80, 79, 81, 17, 80, 81, 79, 72, 17, 79, 81, 116, 73, 314229638, 74, 33614, 75, 56380, 79, 112, 145, 672545599, 84, 2, 61, 5, 79, 6328, 3046477944, 79, 79, 40, 84, 79, 83, 146, 18259, 72, 68, 81, 80, 81, 17, 79, 79, 81, 72, 17, 81, 80, 116, 73, 3770737679, 74, 25285, 75, 3638, 79, 145, 1510703072, 84, 129, 68, 81, 81, 80, 5, 81, 81, 79, 84, 85, 79, 81, 61, 5, 80, 6378, 982859421, 80, 79, 72, 85, 79, 83, 146, 5047, 72, 66, 80, 79, 17, 81, 79, 6396, 72, 80, 80, 83, 17, 79, 79, 79, 116, 73, 2526360450, 74, 4946, 75, 22626, 79, 123, 84, 10, 0, 84, 121, 84, 130, 543037817, 85, 3, 84, 71, 81, 81, 80, 80, 84, 80, 5, 80, 80, 85, 81, 80, 64, 84, 77, 80, 80, 80, 79, 80, 80, 80, 79, 80, 65, 79, 50, 77, 80, 81, 77, 3690386195, 80, 62, 77, 77, 80, 131, 81, 80, 80, 80, 103441029, 81, 80, 115, 73, 3694279092, 78, 3418880806, 66, 81, 79, 78, 80, 80, 6482, 73, 80, 80, 83, 78, 79, 80, 81, 70, 78, 77, 80, 80, 80, 81, 80, 80, 79, 79, 78, 80, 68, 80, 81, 79, 73, 80, 81, 80, 78, 73, 81, 81, 115, 74, 56854, 75, 8615, 79, 146, 44530, 72, 66, 80, 81, 17, 79, 81, 6531, 72, 80, 81, 83, 17, 81, 81, 81, 116, 73, 583156481, 74, 979, 75, 52277, 79, 146, 49437, 72, 66, 81, 80, 17, 80, 81, 6557, 72, 80, 81, 83, 17, 80, 80, 81, 116, 73, 3959377443, 74, 63665, 75, 759, 79, 68, 79, 79, 80, 5, 80, 79, 80, 13, 84, 79, 79, 43, 12, 3, 11, 1, 84, 120, 14, 132, 3944112571, 72, 56025, 68, 81, 80, 79, 17, 79, 80, 79, 72, 17, 80, 79, 116, 73, 1234447863, 74, 6365, 75, 42339, 79, 140, 84, 126, 145068275, 11, 3, 84, 13, 7, 2039942731, 72, 13721, 66, 80, 80, 17, 80, 79, 6636, 72, 80, 81, 83, 17, 81, 80, 80, 116, 73, 1051268365, 74, 12743, 75, 45134, 79, 146, 19679, 72, 66, 81, 80, 17, 80, 81, 6662, 72, 81, 81, 83, 17, 81, 81, 79, 116, 73, 2376286859, 74, 1097, 75, 24916, 79, 61, 3, 81, 6681, 45366795, 81, 80, 5, 6, 79, 83, 145, 4022951899, 84, 133, 61, 4, 79, 6696, 3356184819, 80, 80, 22, 84, 81, 83, 146, 9815, 72, 66, 79, 79, 17, 79, 81, 6714, 72, 79, 81, 83, 17, 79, 79, 79, 116, 73, 143298244, 74, 47120, 75, 58988, 79, 42, 73, 76, 3, 73, 74, 75, 113, 128, 97, 134, 1531491623, 96, 135, 2130553777, 95, 136, 3972638712, 94, 137, 1979006283, 98, 2, 672545599, 47, 90, 11064, 3, 15, 0, 0, 127, 20, 21, 138, 4183768738, 61, 22, 94, 6769, 45366795, 94, 94, 5, 21, 94, 98, 145, 672545599, 23, 2, 61, 24, 95, 6784, 4181491721, 94, 96, 53, 23, 94, 98, 145, 2111003187, 25, 139, 63, 94, 96, 2487545629, 94, 95, 95, 22, 94, 26, 25, 94, 94, 140, 95, 96, 64, 26, 92, 94, 95, 94, 94, 95, 96, 94, 95, 95, 65, 95, 26, 92, 94, 95, 92, 1297108192, 95, 115, 87, 310968969, 93, 496929078, 66, 94, 96, 93, 95, 94, 6845, 87, 95, 95, 98, 93, 95, 95, 95, 67, 96, 141, 93, 93, 4136596108, 94, 95, 92, 94, 68, 95, 95, 94, 87, 94, 95, 96, 93, 87, 95, 95, 115, 88, 4417, 89, 30793, 78, 145, 1657977222, 27, 142, 61, 24, 95, 6888, 2860714620, 95, 94, 29, 27, 94, 98, 69, 94, 22, 94, 40834, 24, 95, 95, 94, 28, 64, 28, 92, 95, 95, 95, 95, 94, 94, 94, 94, 94, 65, 96, 37, 92, 94, 95, 92, 2153582824, 96, 62, 92, 92, 94, 143, 95, 96, 96, 95, 2713432914, 96, 94, 115, 87, 1979422889, 93, 1954984988, 66, 95, 95, 93, 94, 95, 6951, 87, 96, 94, 98, 93, 96, 95, 96, 70, 93, 92, 95, 95, 94, 94, 96, 94, 95, 95, 93, 95, 68, 96, 95, 96, 87, 95, 95, 95, 93, 87, 95, 96, 115, 88, 44600, 89, 51365, 78, 116, 87, 748868035, 88, 64607, 89, 14832, 78, 145, 2488547328, 29, 144, 66, 94, 95, 30, 96, 95, 7009, 29, 95, 94, 98, 24, 95, 95, 96, 61, 24, 94, 7020, 1341999285, 94, 94, 16, 30, 96, 98, 69, 96, 22, 94, 19124, 24, 94, 94, 96, 31, 64, 31, 92, 94, 94, 96, 94, 95, 94, 95, 95, 95, 65, 95, 22, 92, 96, 96, 92, 3356184819, 95, 62, 92, 92, 95, 145, 95, 94, 95, 96, 3025079268, 94, 95, 115, 87, 2897312472, 93, 2632456081, 66, 95, 95, 93, 95, 95, 7083, 87, 95, 96, 98, 93, 94, 95, 94, 70, 93, 92, 94, 95, 94, 94, 95, 96, 94, 95, 93, 96, 68, 94, 96, 96, 87, 94, 94, 95, 93, 87, 96, 94, 115, 88, 41877, 89, 3069, 78, 116, 87, 2073485759, 88, 39246, 89, 31259, 78, 145, 454845331, 32, 146, 68, 94, 94, 96, 24, 95, 96, 94, 32, 33, 94, 94, 61, 24, 96, 7149, 2153582824, 94, 96, 37, 33, 95, 98, 69, 94, 22, 96, 28227, 24, 94, 96, 94, 34, 64, 34, 92, 96, 96, 94, 94, 95, 96, 95, 94, 94, 65, 94, 53, 92, 96, 94, 92, 4181491721, 95, 62, 92, 92, 95, 147, 94, 95, 95, 94, 3042864084, 95, 94, 115, 87, 185606065, 93, 2038365596, 66, 96, 95, 93, 95, 95, 7212, 87, 95, 94, 98, 93, 95, 95, 95, 70, 93, 93, 94, 95, 94, 96, 94, 96, 95, 95, 92, 96, 68, 96, 96, 95, 87, 95, 94, 95, 93, 87, 96, 94, 115, 88, 5171, 89, 49883, 78, 116, 87, 1845742283, 88, 34861, 89, 7414, 78, 145, 1971642377, 35, 148, 68, 96, 95, 94, 24, 94, 94, 94, 35, 36, 94, 95, 61, 24, 96, 7278, 1297108192, 94, 94, 26, 36, 95, 98, 69, 95, 22, 96, 9462, 24, 94, 95, 95, 37, 64, 37, 92, 96, 94, 96, 96, 95, 94, 94, 94, 96, 65, 95, 58, 92, 96, 94, 92, 3494415691, 94, 115, 87, 1856406503, 93, 1273882382, 66, 94, 96, 93, 94, 94, 7329, 87, 96, 94, 98, 93, 94, 95, 95, 67, 95, 149, 93, 92, 35151454, 96, 95, 93, 95, 68, 95, 94, 96, 87, 94, 94, 94, 93, 87, 95, 94, 115, 88, 21011, 89, 18789, 78, 116, 87, 3541563335, 88, 18676, 89, 2521, 78, 145, 275602057, 38, 150, 66, 94, 96, 39, 95, 96, 7384, 38, 95, 94, 98, 24, 96, 95, 96, 61, 24, 96, 7395, 982859421, 94, 96, 72, 39, 96, 98, 69, 94, 22, 96, 38103, 24, 94, 94, 96, 40, 64, 40, 92, 96, 96, 94, 96, 95, 94, 94, 96, 96, 65, 95, 30, 92, 94, 95, 92, 2249185396, 95, 62, 92, 92, 95, 151, 95, 96, 96, 96, 3146847877, 95, 95, 115, 87, 3203871219, 93, 2932790955, 66, 95, 96, 93, 96, 95, 7458, 87, 95, 95, 98, 93, 96, 96, 96, 70, 93, 92, 96, 96, 94, 96, 95, 94, 96, 96, 93, 95, 68, 96, 95, 94, 87, 95, 95, 94, 93, 87, 94, 95, 115, 88, 45074, 89, 44490, 78, 116, 87, 1667370848, 88, 26636, 89, 65206, 78, 145, 3680779894, 41, 152, 68, 94, 95, 96, 24, 95, 95, 96, 41, 42, 94, 95, 61, 24, 94, 7524, 1202307030, 96, 94, 93, 42, 94, 98, 69, 95, 22, 96, 62762, 24, 95, 94, 94, 43, 64, 43, 92, 94, 94, 94, 94, 94, 96, 96, 96, 95, 65, 96, 58, 92, 94, 96, 92, 3494415691, 95, 62, 92, 92, 96, 153, 95, 94, 96, 96, 3475661521, 95, 94, 115, 87, 3214521289, 93, 2430785428, 66, 96, 95, 93, 95, 94, 7587, 87, 96, 96, 98, 93, 95, 95, 95, 70, 93, 93, 96, 95, 95, 95, 96, 95, 96, 96, 92, 95, 68, 95, 95, 95, 87, 95, 94, 96, 93, 87, 95, 94, 115, 88, 25088, 89, 5462, 78, 116, 87, 14121355, 88, 14490, 89, 26865, 78, 145, 3075910129, 44, 154, 66, 94, 96, 45, 96, 95, 7645, 44, 94, 94, 98, 24, 95, 95, 95, 61, 24, 94, 7656, 1297108192, 94, 95, 26, 45, 96, 98, 69, 95, 22, 96, 5781, 24, 94, 95, 95, 46, 64, 46, 92, 94, 94, 94, 95, 95, 95, 95, 94, 95, 65, 96, 30, 92, 96, 96, 92, 2249185396, 96, 115, 87, 1765164946, 93, 1489970889, 66, 95, 95, 93, 96, 95, 7707, 87, 95, 95, 98, 93, 94, 94, 95, 67, 96, 155, 93, 92, 4188226358, 96, 96, 93, 96, 68, 96, 96, 95, 87, 94, 94, 96, 93, 87, 96, 96, 115, 88, 38293, 89, 33320, 78, 116, 87, 877231519, 88, 29029, 89, 20729, 78, 145, 3251957914, 47, 156, 68, 94, 95, 95, 24, 96, 94, 96, 47, 48, 96, 94, 61, 24, 95, 7770, 4181491721, 96, 94, 53, 48, 95, 98, 69, 94, 22, 94, 47729, 24, 95, 94, 94, 49, 64, 49, 92, 94, 94, 94, 94, 94, 94, 96, 94, 94, 65, 94, 53, 92, 96, 96, 92, 4181491721, 95, 115, 87, 2135269589, 93, 1872643070, 66, 96, 96, 93, 96, 94, 7821, 87, 95, 96, 98, 93, 95, 94, 95, 67, 95, 157, 93, 93, 347934106, 94, 95, 92, 94, 68, 94, 94, 96, 87, 96, 96, 96, 93, 87, 95, 94, 115, 88, 9058, 89, 49355, 78, 116, 87, 168496557, 88, 35554, 89, 4507, 78, 145, 512644231, 50, 158, 68, 95, 94, 94, 24, 95, 96, 95, 50, 51, 96, 95, 61, 24, 95, 7884, 2089930795, 95, 96, 15, 51, 96, 98, 69, 94, 22, 95, 43967, 24, 94, 94, 96, 52, 64, 52, 92, 96, 95, 95, 96, 95, 95, 94, 95, 94, 65, 96, 43, 92, 96, 95, 92, 3403343297, 96, 62, 92, 92, 94, 159, 95, 95, 94, 96, 2422014050, 95, 94, 115, 87, 643765595, 93, 915831188, 66, 95, 95, 93, 96, 94, 7947, 87, 95, 95, 98, 93, 95, 94, 94, 70, 93, 92, 95, 96, 96, 96, 94, 96, 95, 95, 93, 96, 68, 96, 95, 95, 87, 95, 96, 96, 93, 87, 96, 95, 115, 88, 49239, 89, 29771, 78, 116, 87, 2924899641, 88, 32768, 89, 33647, 78, 145, 1681994414, 53, 160, 66, 95, 94, 54, 95, 94, 8005, 53, 95, 94, 98, 24, 94, 96, 96, 61, 24, 96, 8016, 1297108192, 95, 95, 26, 54, 96, 98, 69, 96, 22, 95, 60014, 24, 94, 94, 95, 55, 64, 55, 92, 94, 96, 94, 95, 94, 96, 96, 95, 94, 65, 96, 40, 92, 94, 95, 92, 3046477944, 94, 62, 92, 92, 94, 161, 94, 96, 96, 96, 2483951636, 95, 94, 115, 87, 1919406065, 93, 581971258, 66, 96, 95, 93, 96, 94, 8079, 87, 96, 95, 98, 93, 95, 94, 94, 70, 93, 93, 95, 95, 95, 96, 95, 96, 95, 96, 92, 96, 68, 95, 95, 96, 87, 96, 94, 96, 93, 87, 95, 96, 115, 88, 22303, 89, 9447, 78, 116, 87, 1735432573, 88, 408, 89, 33343, 78, 145, 3592243542, 56, 162, 66, 96, 94, 57, 96, 96, 8137, 56, 96, 96, 98, 24, 96, 96, 96, 61, 24, 94, 8148, 3046477944, 94, 94, 40, 57, 94, 98, 69, 94, 22, 96, 58866, 24, 94, 96, 94, 58, 64, 58, 92, 95, 94, 94, 94, 94, 95, 94, 96, 94, 65, 96, 25, 92, 95, 95, 92, 2989996262, 95, 115, 87, 4264152298, 93, 3982635695, 66, 96, 96, 93, 95, 94, 8199, 87, 96, 96, 98, 93, 94, 94, 96, 67, 94, 163, 93, 93, 251766634, 94, 96, 92, 96, 68, 94, 95, 95, 87, 94, 95, 96, 93, 87, 95, 94, 115, 88, 20604, 89, 33915, 78, 116, 87, 3447247475, 88, 46318, 89, 42930, 78, 145, 2066415669, 59, 164, 66, 94, 95, 60, 95, 95, 8254, 59, 94, 95, 98, 24, 95, 95, 94, 61, 24, 95, 8265, 3403343297, 96, 95, 43, 60, 95, 98, 69, 95, 22, 94, 42749, 24, 94, 94, 94, 61, 64, 61, 92, 94, 95, 94, 95, 95, 95, 96, 96, 95, 65, 95, 22, 92, 94, 95, 92, 3356184819, 94, 62, 92, 92, 95, 165, 94, 95, 95, 94, 4057917647, 96, 94, 115, 87, 1371318654, 93, 1659997503, 66, 95, 95, 93, 94, 95, 8328, 87, 95, 95, 98, 93, 95, 95, 96, 70, 93, 92, 95, 94, 94, 94, 94, 96, 96, 96, 93, 95, 68, 96, 95, 96, 87, 95, 94, 96, 93, 87, 96, 96, 115, 88, 32794, 89, 48502, 78, 116, 87, 4096197882, 88, 56470, 89, 26031, 78, 145, 3823598138, 62, 166, 66, 94, 94, 63, 95, 94, 8386, 62, 94, 94, 98, 24, 94, 95, 94, 61, 24, 95, 8397, 2249185396, 94, 96, 30, 63, 94, 98, 69, 94, 22, 96, 3065, 24, 94, 95, 95, 64, 64, 64, 92, 94, 94, 94, 95, 94, 94, 96, 96, 95, 65, 96, 16, 92, 94, 96, 92, 1341999285, 95, 62, 92, 92, 95, 167, 95, 95, 96, 95, 1406249635, 95, 96, 115, 87, 3689163774, 93, 3954006163, 66, 94, 94, 93, 95, 96, 8460, 87, 95, 95, 98, 93, 95, 95, 95, 70, 93, 93, 95, 95, 94, 95, 94, 95, 95, 96, 92, 95, 68, 95, 96, 94, 87, 95, 95, 95, 93, 87, 96, 96, 115, 88, 59268, 89, 45858, 78, 116, 87, 1492154942, 88, 14512, 89, 63414, 78, 145, 3896946603, 65, 168, 68, 95, 94, 95, 24, 94, 94, 94, 65, 66, 94, 94, 61, 24, 95, 8526, 3356184819, 95, 95, 22, 66, 95, 98, 69, 94, 22, 95, 59240, 24, 94, 96, 95, 67, 64, 67, 92, 95, 95, 94, 95, 94, 96, 95, 94, 96, 65, 94, 61, 92, 94, 96, 92, 1960262425, 96, 62, 92, 92, 94, 169, 96, 94, 95, 95, 3635747604, 95, 94, 115, 87, 681999006, 93, 3523034487, 66, 96, 96, 93, 94, 95, 8589, 87, 94, 96, 98, 93, 95, 95, 95, 70, 93, 93, 95, 96, 95, 95, 95, 96, 96, 94, 92, 94, 68, 96, 96, 96, 87, 95, 96, 95, 93, 87, 96, 95, 115, 88, 20899, 89, 9067, 78, 116, 87, 444568800, 88, 26907, 89, 45099, 78, 145, 3840523919, 68, 170, 66, 96, 94, 69, 96, 96, 8647, 68, 96, 96, 98, 24, 96, 96, 96, 61, 24, 94, 8658, 3403343297, 94, 95, 43, 69, 96, 98, 69, 96, 22, 94, 41207, 24, 95, 95, 96, 70, 64, 70, 92, 96, 95, 95, 94, 96, 96, 95, 96, 94, 65, 96, 53, 92, 94, 94, 92, 4181491721, 94, 115, 87, 613866215, 93, 341825456, 66, 94, 94, 93, 95, 94, 8709, 87, 96, 94, 98, 93, 95, 94, 95, 67, 95, 171, 93, 92, 422554534, 94, 95, 93, 95, 68, 95, 95, 96, 87, 96, 95, 94, 93, 87, 96, 94, 115, 88, 52900, 89, 11696, 78, 116, 87, 3142329944, 88, 39943, 89, 42661, 78, 145, 3380613112, 71, 32, 66, 94, 96, 72, 96, 94, 8764, 71, 96, 95, 98, 24, 94, 94, 94, 61, 24, 96, 8775, 3690386195, 96, 95, 50, 72, 94, 98, 69, 96, 22, 94, 28776, 24, 95, 95, 96, 73, 64, 73, 92, 96, 94, 96, 94, 94, 95, 95, 95, 96, 65, 96, 15, 92, 94, 94, 92, 2089930795, 95, 62, 92, 92, 94, 172, 94, 94, 96, 95, 1066902891, 94, 95, 115, 87, 2178900713, 93, 1377173298, 66, 96, 95, 93, 94, 94, 8838, 87, 96, 96, 98, 93, 94, 94, 94, 70, 93, 92, 94, 96, 94, 96, 94, 96, 94, 94, 93, 96, 68, 95, 96, 94, 87, 96, 96, 96, 93, 87, 94, 96, 115, 88, 48820, 89, 23501, 78, 116, 87, 243825728, 88, 22226, 89, 22867, 78, 145, 1197694456, 74, 173, 66, 94, 96, 75, 95, 95, 8896, 74, 94, 94, 98, 24, 95, 95, 95, 61, 24, 95, 8907, 3356184819, 95, 95, 22, 75, 95, 98, 69, 95, 22, 96, 15726, 24, 95, 96, 95, 76, 64, 76, 92, 96, 96, 95, 96, 95, 96, 94, 96, 94, 65, 94, 40, 92, 95, 94, 92, 3046477944, 94, 62, 92, 92, 95, 174, 94, 95, 95, 96, 2415292772, 94, 96, 115, 87, 450130952, 93, 185309119, 66, 95, 95, 93, 95, 96, 8970, 87, 96, 94, 98, 93, 95, 94, 95, 70, 93, 93, 94, 96, 94, 95, 94, 94, 96, 95, 92, 95, 68, 95, 95, 94, 87, 96, 94, 96, 93, 87, 94, 95, 115, 88, 64169, 89, 3673, 78, 116, 87, 3820695545, 88, 36351, 89, 60260, 78, 145, 3930375624, 77, 175, 68, 94, 94, 94, 24, 94, 95, 95, 77, 78, 95, 94, 61, 24, 95, 9036, 1202307030, 96, 95, 93, 78, 95, 98, 69, 94, 22, 94, 41287, 24, 94, 95, 95, 79, 64, 79, 92, 94, 94, 94, 94, 95, 96, 94, 96, 94, 65, 95, 15, 92, 94, 94, 92, 2089930795, 94, 115, 87, 609248712, 93, 343886368, 66, 96, 94, 93, 96, 96, 9087, 87, 96, 94, 98, 93, 94, 94, 95, 67, 95, 176, 93, 93, 462192030, 96, 95, 92, 95, 68, 94, 94, 94, 87, 94, 96, 94, 93, 87, 94, 94, 115, 88, 16550, 89, 60347, 78, 116, 87, 2946031097, 88, 12983, 89, 37805, 78, 145, 348192342, 80, 177, 66, 96, 94, 81, 94, 96, 9142, 80, 96, 96, 98, 24, 96, 96, 95, 61, 24, 95, 9153, 3356184819, 94, 96, 22, 81, 96, 98, 69, 94, 22, 94, 55709, 24, 95, 94, 95, 82, 64, 82, 92, 96, 94, 94, 94, 94, 95, 95, 94, 94, 65, 96, 43, 92, 94, 95, 92, 3403343297, 96, 115, 87, 1494463956, 93, 1751030429, 66, 94, 96, 93, 94, 95, 9204, 87, 95, 94, 98, 93, 96, 95, 95, 67, 94, 178, 93, 92, 3815112899, 96, 95, 93, 95, 68, 95, 96, 95, 87, 95, 94, 96, 93, 87, 94, 96, 115, 88, 37251, 89, 29761, 78, 116, 87, 3694802045, 88, 22837, 89, 34873, 78, 145, 497481819, 83, 179, 68, 94, 94, 94, 24, 94, 94, 94, 83, 84, 94, 95, 61, 24, 96, 9267, 1960262425, 94, 94, 61, 84, 96, 98, 69, 95, 22, 95, 59214, 24, 95, 95, 95, 85, 64, 85, 92, 96, 95, 94, 95, 94, 96, 95, 94, 95, 65, 94, 61, 92, 95, 95, 92, 1960262425, 96, 115, 87, 2892139408, 93, 1561982921, 66, 96, 94, 93, 94, 94, 9318, 87, 95, 96, 98, 93, 94, 95, 96, 67, 94, 180, 93, 92, 441562625, 95, 94, 93, 95, 68, 94, 94, 96, 87, 95, 96, 95, 93, 87, 95, 95, 115, 88, 18229, 89, 42911, 78, 116, 87, 729096824, 88, 3920, 89, 53526, 78, 116, 87, 3835591031, 88, 20021, 89, 22059, 78, 61, 6, 94, 9373, 3356184819, 94, 94, 22, 18, 95, 98, 146, 9334, 86, 66, 94, 96, 22, 94, 94, 9391, 86, 96, 94, 98, 22, 95, 95, 95, 116, 87, 2458596485, 88, 2363, 89, 33846, 78, 140, 99, 127, 732436688, 15, 14, 99, 16, 126, 145068275, 86, 7665, 68, 95, 95, 96, 22, 94, 95, 95, 86, 22, 95, 94, 116, 87, 3454847284, 88, 65256, 89, 16856, 78, 146, 53295, 86, 71, 96, 95, 96, 96, 86, 96, 22, 96, 96, 86, 94, 94, 64, 86, 92, 96, 96, 96, 95, 94, 96, 95, 96, 94, 65, 94, 5, 92, 95, 96, 92, 45366795, 94, 62, 92, 92, 94, 181, 95, 96, 96, 96, 2697819037, 96, 94, 115, 87, 3183626288, 93, 3495942054, 66, 96, 96, 93, 95, 96, 9500, 87, 94, 94, 98, 93, 96, 94, 96, 70, 93, 93, 96, 94, 96, 95, 96, 95, 96, 95, 92, 94, 68, 95, 95, 94, 87, 96, 96, 94, 93, 87, 95, 95, 115, 88, 48620, 89, 25241, 78, 146, 40737, 86, 68, 95, 95, 95, 22, 94, 96, 96, 86, 22, 96, 95, 116, 87, 4122932106, 88, 51206, 89, 30986, 78, 146, 17102, 86, 68, 94, 95, 94, 22, 94, 94, 95, 86, 22, 94, 96, 116, 87, 1542397087, 88, 4381, 89, 57306, 78, 145, 1298030640, 99, 182, 68, 95, 95, 95, 1, 94, 95, 96, 99, 9, 94, 95, 120, 10, 2, 672545599, 86, 29656, 68, 95, 95, 95, 22, 95, 95, 95, 86, 22, 94, 96, 116, 87, 3317467851, 88, 48961, 89, 18696, 78, 145, 672545599, 99, 2, 61, 7, 96, 9634, 3101106206, 96, 94, 19, 99, 94, 98, 146, 52688, 86, 66, 95, 95, 22, 96, 94, 9652, 86, 96, 96, 98, 22, 94, 96, 95, 116, 87, 3542421899, 88, 9729, 89, 17552, 78, 147, 6, 146, 34188, 86, 66, 95, 94, 22, 95, 95, 9680, 86, 95, 96, 98, 22, 95, 94, 94, 116, 87, 1291992545, 88, 29279, 89, 52943, 78, 150, 0, 16, 99, 68, 96, 96, 95, 2, 95, 96, 96, 7, 100, 96, 95, 43, 17, 0, 99, 1, 100, 146, 38711, 86, 68, 96, 96, 95, 22, 94, 95, 96, 86, 22, 94, 96, 116, 87, 2614727603, 88, 43113, 89, 40935, 78, 61, 7, 94, 9744, 2860714620, 94, 96, 29, 19, 96, 98, 146, 15041, 86, 66, 95, 94, 22, 94, 94, 9762, 86, 96, 94, 98, 22, 94, 94, 94, 116, 87, 908525522, 88, 13390, 89, 14738, 78, 74, 17, 94, 99, 94, 96, 8, 95, 96, 96, 94, 94, 94, 95, 43, 100, 14, 15, 1, 99, 68, 96, 95, 95, 6, 96, 95, 94, 100, 18, 95, 94, 146, 6656, 86, 66, 95, 96, 22, 96, 95, 9820, 86, 95, 96, 98, 22, 96, 95, 95, 116, 87, 3690863898, 88, 4758, 89, 24523, 78, 73, 2860714620, 10, 26, 906455654, 95, 183, 1297108192, 96, 9, 99, 96, 96, 94, 29, 95, 61, 1, 95, 9855, 1341999285, 95, 96, 16, 99, 96, 98, 120, 11, 184, 1291331714, 86, 29776, 68, 94, 96, 94, 22, 96, 96, 95, 86, 22, 96, 96, 116, 87, 730803665, 88, 591, 89, 12090, 78, 123, 99, 10, 0, 99, 75, 96, 14948, 99, 94, 11, 94, 96, 94, 94, 1, 96, 74, 1, 96, 12, 95, 95, 99, 95, 94, 96, 94, 95, 94, 95, 120, 13, 185, 4285416959, 86, 41039, 66, 95, 95, 22, 96, 96, 9933, 86, 94, 96, 98, 22, 96, 96, 96, 116, 87, 2975117817, 88, 34743, 89, 42336, 78, 127, 5, 99, 133, 4022951899, 61, 6, 96, 9957, 1297108192, 94, 95, 26, 99, 96, 98, 146, 4490, 86, 66, 94, 94, 22, 95, 95, 9975, 86, 96, 94, 98, 22, 96, 95, 94, 116, 87, 1412974637, 88, 32227, 89, 51077, 78, 146, 38050, 86, 66, 96, 96, 22, 96, 94, 10001, 86, 95, 96, 98, 22, 94, 94, 96, 116, 87, 4141540161, 88, 55551, 89, 30950, 78, 71, 96, 96, 94, 95, 99, 94, 7, 95, 96, 3, 94, 95, 64, 99, 92, 96, 96, 94, 96, 96, 96, 94, 94, 95, 65, 96, 5, 92, 94, 96, 92, 45366795, 94, 62, 92, 92, 95, 186, 95, 95, 96, 96, 2935942210, 95, 94, 115, 87, 3303415527, 93, 1946398083, 66, 95, 96, 93, 94, 96, 10075, 87, 96, 96, 98, 93, 94, 96, 94, 70, 93, 92, 94, 95, 95, 95, 96, 94, 95, 96, 93, 95, 68, 96, 95, 95, 87, 96, 96, 94, 93, 87, 96, 94, 115, 88, 62794, 89, 37580, 78, 146, 20898, 86, 68, 94, 94, 96, 22, 94, 94, 94, 86, 22, 94, 94, 116, 87, 1782725597, 88, 28755, 89, 60815, 78, 146, 117, 86, 68, 95, 94, 95, 22, 94, 94, 94, 86, 22, 94, 94, 116, 87, 25044605, 88, 6963, 89, 30027, 78, 61, 99, 95, 10163, 2249185396, 95, 95, 30, 7, 95, 98, 145, 2039942731, 99, 7, 68, 96, 95, 96, 7, 95, 96, 95, 99, 19, 96, 95, 146, 17770, 86, 66, 94, 94, 22, 95, 96, 10197, 86, 95, 96, 98, 22, 96, 96, 95, 116, 87, 935828958, 88, 37458, 89, 15349, 78, 146, 47109, 86, 66, 95, 95, 22, 96, 96, 10223, 86, 96, 94, 98, 22, 96, 96, 95, 116, 87, 3733285526, 88, 24330, 89, 20528, 78, 146, 42466, 86, 71, 94, 94, 95, 95, 86, 95, 22, 95, 95, 86, 94, 95, 64, 86, 92, 95, 96, 96, 94, 94, 94, 94, 94, 96, 65, 96, 72, 92, 94, 94, 92, 982859421, 96, 62, 92, 92, 94, 187, 95, 96, 96, 96, 3724215909, 94, 96, 115, 87, 3069583117, 93, 1819258350, 66, 94, 96, 93, 96, 96, 10300, 87, 96, 96, 98, 93, 95, 94, 96, 70, 93, 92, 95, 94, 95, 95, 96, 95, 95, 95, 93, 96, 68, 96, 94, 96, 87, 95, 96, 96, 93, 87, 96, 95, 115, 88, 62621, 89, 46290, 78, 146, 25514, 86, 68, 95, 95, 95, 22, 94, 96, 94, 86, 22, 94, 96, 116, 87, 2179071106, 88, 12606, 89, 62959, 78, 146, 15525, 86, 66, 94, 96, 22, 95, 95, 10372, 86, 95, 95, 98, 22, 95, 95, 95, 116, 87, 141807466, 88, 58406, 89, 2625, 78, 70, 99, 13, 96, 96, 96, 95, 96, 96, 96, 96, 12, 94, 61, 8, 96, 10404, 3690386195, 95, 96, 50, 99, 96, 98, 148, 14, 45366795, 5, 146, 16085, 86, 66, 96, 95, 22, 96, 94, 10426, 86, 96, 96, 98, 22, 96, 96, 94, 116, 87, 436506859, 88, 42657, 89, 7600, 78, 42, 87, 90, 3, 87, 88, 89, 121, 91, 188, 1656634012, 87, 87, 91, 114, 129, 6, 189, 2724556, 5, 190, 919364413, 4, 191, 2689949339, 7, 2, 672545599, 3, 76, 4, 4, 4, 8, 5, 1, 5, 4, 4, 4, 145, 672545599, 9, 2, 75, 5, 2323, 10, 4, 9, 5, 5, 4, 5, 8, 4, 61, 1, 4, 10501, 3046477944, 5, 5, 40, 10, 4, 7, 130, 8, 37, 2153582824, 9, 192, 2617990941, 10, 8, 9, 11, 193, 3390074458, 12, 8, 10, 2, 1, 11, 145, 672545599, 8, 2, 75, 4, 17420, 9, 4, 8, 4, 5, 4, 4, 12, 4, 61, 1, 5, 10547, 45366795, 4, 5, 5, 9, 4, 7, 145, 2010510042, 8, 194, 68, 4, 5, 4, 1, 5, 4, 5, 8, 9, 5, 5, 145, 672545599, 8, 2, 75, 5, 35774, 10, 5, 8, 4, 4, 5, 5, 9, 4, 61, 1, 4, 10590, 45366795, 4, 5, 5, 10, 4, 7, 145, 1695563136, 8, 195, 74, 1, 5, 9, 4, 5, 8, 5, 4, 4, 5, 4, 4, 4, 145, 672545599, 8, 2, 75, 5, 37642, 10, 4, 8, 4, 5, 4, 4, 9, 5, 61, 1, 5, 10635, 3101106206, 5, 5, 19, 10, 4, 7, 145, 1063960183, 8, 196, 75, 5, 28041, 9, 4, 8, 5, 4, 5, 5, 1, 5, 74, 1, 5, 8, 5, 5, 9, 4, 4, 5, 5, 5, 5, 5, 145, 672545599, 9, 2, 75, 4, 8259, 10, 4, 9, 4, 4, 4, 4, 8, 5, 61, 1, 5, 10692, 2153582824, 4, 4, 37, 10, 4, 7, 74, 1, 4, 8, 5, 4, 0, 5, 5, 4, 5, 5, 5, 4, 145, 672545599, 9, 2, 75, 4, 23635, 10, 5, 9, 5, 5, 5, 5, 8, 4, 61, 1, 4, 10733, 3494415691, 5, 4, 58, 10, 4, 7, 48, 8, 1, 1, 147, 8, 123, 8, 10, 0, 8, 128, 6, 197, 945014733, 7, 198, 4076866912, 9, 199, 1497848766, 8, 196, 1063960183, 10, 2, 672545599, 127, 4, 11, 200, 3201057905, 72, 10, 6, 7, 8, 10780, 8, 11, 2, 7, 8, 8, 6, 12, 8, 73, 3356184819, 12, 40, 3035303800, 7, 201, 3046477944, 8, 1, 11, 6, 7, 8, 22, 8, 145, 672545599, 12, 2, 75, 6, 13925, 13, 8, 12, 7, 7, 6, 6, 11, 6, 61, 5, 7, 10823, 1297108192, 8, 8, 26, 13, 8, 10, 145, 945622063, 11, 202, 66, 8, 8, 12, 8, 7, 10842, 11, 8, 8, 10, 0, 8, 8, 7, 145, 672545599, 11, 2, 75, 6, 35148, 13, 6, 11, 8, 8, 8, 8, 12, 7, 61, 0, 8, 10869, 3046477944, 8, 7, 40, 13, 7, 10, 68, 8, 7, 7, 0, 7, 7, 7, 5, 11, 8, 7, 145, 672545599, 12, 2, 75, 8, 3660, 13, 7, 12, 8, 8, 7, 7, 11, 7, 61, 0, 8, 10908, 4181491721, 7, 7, 53, 13, 7, 10, 145, 522764395, 11, 203, 66, 7, 6, 12, 7, 7, 10927, 11, 7, 7, 10, 0, 7, 8, 7, 145, 672545599, 11, 2, 75, 7, 23577, 13, 7, 11, 7, 7, 8, 8, 12, 7, 61, 0, 8, 10954, 4181491721, 7, 7, 53, 13, 6, 10, 145, 2995432582, 11, 204, 75, 8, 53160, 12, 6, 11, 7, 7, 7, 8, 0, 7, 74, 0, 8, 11, 7, 6, 12, 7, 6, 7, 7, 7, 8, 8, 145, 672545599, 12, 2, 75, 8, 22434, 13, 8, 12, 6, 8, 8, 6, 11, 7, 61, 0, 6, 11011, 2860714620, 8, 8, 29, 13, 7, 10, 130, 11, 37, 2153582824, 12, 192, 2617990941, 13, 11, 12, 14, 205, 2163802649, 15, 11, 13, 2, 0, 14, 145, 672545599, 11, 2, 75, 7, 4648, 12, 8, 11, 8, 6, 7, 8, 15, 8, 61, 0, 8, 11057, 1202307030, 8, 7, 93, 12, 8, 10, 147, 0, 123, 11, 10, 0, 11, 129, 7, 206, 3530899648, 6, 207, 3028146687, 8, 208, 3859288425, 9, 2, 672545599, 4, 145, 3201057905, 10, 200, 72, 9, 6, 7, 7, 11097, 7, 10, 2, 7, 7, 7, 7, 11, 6, 73, 1960262425, 11, 15, 653265000, 7, 209, 2089930795, 7, 1, 10, 6, 6, 7, 61, 7, 145, 672545599, 11, 2, 75, 7, 44552, 12, 7, 11, 6, 7, 6, 7, 10, 6, 61, 5, 6, 11140, 3046477944, 7, 7, 40, 12, 6, 9, 145, 372135464, 10, 210, 68, 6, 7, 7, 0, 7, 6, 7, 10, 11, 7, 7, 145, 672545599, 10, 2, 75, 6, 21514, 12, 6, 10, 6, 6, 7, 7, 11, 7, 61, 0, 6, 11183, 1202307030, 6, 6, 93, 12, 7, 9, 145, 4248988983, 10, 211, 68, 7, 7, 7, 0, 7, 7, 7, 10, 11, 7, 7, 145, 672545599, 10, 2, 75, 6, 26596, 12, 7, 10, 7, 6, 6, 6, 11, 7, 61, 0, 7, 11226, 3494415691, 7, 6, 58, 12, 6, 9, 76, 6, 6, 7, 10, 6, 0, 6, 7, 7, 7, 145, 672545599, 11, 2, 75, 7, 53095, 12, 7, 11, 6, 7, 7, 6, 10, 7, 61, 0, 6, 11264, 2089930795, 6, 7, 15, 12, 6, 9, 74, 0, 6, 10, 6, 6, 5, 6, 7, 7, 6, 7, 7, 6, 145, 672545599, 11, 2, 75, 6, 13371, 12, 6, 11, 6, 6, 7, 7, 10, 6, 61, 0, 7, 11305, 3403343297, 7, 7, 43, 12, 7, 9, 145, 1063960183, 10, 196, 75, 6, 26297, 11, 6, 10, 7, 6, 7, 7, 0, 6, 74, 0, 7, 10, 6, 7, 11, 6, 6, 6, 6, 7, 6, 6, 145, 672545599, 11, 2, 75, 7, 56329, 12, 6, 11, 6, 6, 6, 6, 10, 7, 61, 0, 6, 11362, 4181491721, 6, 6, 53, 12, 6, 9, 130, 10, 37, 2153582824, 11, 192, 2617990941, 12, 10, 11, 13, 212, 2394619106, 14, 10, 12, 2, 0, 13, 145, 672545599, 10, 2, 75, 6, 52275, 11, 6, 10, 6, 6, 6, 6, 14, 6, 61, 0, 7, 11408, 3690386195, 6, 6, 50, 11, 6, 9, 145, 1656634012, 10, 188, 49, 11, 1, 10, 0, 147, 11, 123, 10, 10, 0, 10]; +VM.prototype.Z = function (fp, fake_26, slot, fake_25) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.f[fp + 7] + slot; + var uvs = this.I || (this.I = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(null, this.f, absSlot, [])); + this.w = fake_26 || fake_25; +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +Upvalue.prototype.U = function (v, fake_8, fake_7) { + this.ae = fake_7 && fake_8; + if (this.x) this.h = v;else this.f[this.F] = v; +}; +VM.prototype.M = function (fake_18, fake_19, fake_20, fake_17) { + this.C = fake_17 && fake_19; + this.r = [fake_18, fake_20]; + return this.ag[this.f[this.y + 0]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +Upvalue.prototype.G = function (fake_10, fake_9) { + this.X = fake_9 && fake_10; + this.h = this.f[this.F]; + this.x = true; +}; +Upvalue.prototype.d = function (fake_4, fake_5, fake_3, fake_6) { + this.ab = fake_5; + this.aa = [fake_6, fake_4]; + return this.x ? this.h : this.f[this.F]; + this.A = fake_3; +}; +VM.prototype.g = function (idxIn, fake_27, fake_29, fake_28, keyIn, fake_30) { + var idx = idxIn ?? this.M(undefined, {}, undefined, "x"); + var key = keyIn ?? this.M(67, [], null, {}); + var v = this.a[idx]; + this.Y = [fake_30, fake_29]; + if (!key) return v; + this.b = [fake_27, fake_28]; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.N = function (fp, fake_34, fake_33, fake_32, fake_31) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.I; + this.P = fake_33; + this.ai = fake_34; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this.f; + var hi = fp + regs[fp + 6]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.G("y", 68); + uvs[i] = undefined; + } + } + this.q = fake_31; + this.o = fake_32; +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.j = function (args, fake_24, fake_23, fake_21, closure, thisVal, retDst, fake_22) { + var fn = closure.R; + var regs = this.f; + var fp = this.z; + var size = 8 + fn.af; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 8; + regs[fp + 0] = fn.ad; + regs[fp + 1] = this.y; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.H; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.ad; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.Q = fake_24 && fake_22; + this.l = fake_23; + this.z = end; + if (args) { + if (fn.E) { + var restSlot = fn.H - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.af; a++) regs[base + a] = args[a]; + } + if (fn.H < fn.af) regs[base + fn.H] = args; + } + this.y = fp; + this.n = fake_21; +}; +VM.prototype.L = function (args, fake_37, fake_36, fake_38, thisVal, fake_35, closure) { + var now = () => { + return performance.now(); + }; + this.p = fake_35 && fake_38; + var lastTime = now(); + this.j(args, 68, undefined, [], closure, thisVal, 0, "z"); + this.B = fake_37; + while (true) { + var fp = this.y; + var regs = this.f; + var bc = this.ag; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.ag[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.ag.length; i++) this.ag[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.z; i2++) this.f[i2] = undefined; + op = 39; + regs[fp + 0] = this.ag.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + this[op](); + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.y; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.N(searchFrame, 67, [], "q", null); + this.z = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.y = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.e !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.e] = err; + regs[handledFrame + 0] = h.k; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.T] = h.c; + regs[hBase + h.s] = err; + regs[handledFrame + 0] = h.m; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.z = handledFrame + regs[handledFrame + 6]; + this.y = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.y) return this.f[0]; + } + this.t = fake_36; +}; + +/* @BOOT */ // <- This comment can't be removed! +var VMPrototype = VM.prototype; +VMPrototype[136] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this.M("y", 46, undefined, 73); + regs[base + dst] = this.g(); + // LOAD_THIS + var dst = this.M({}, {}, 73, {}); + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[69] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_EQ_2_7_0_1_8_5_4_3_6 (order: [2,7,0,1,8,5,4,3,6]) + let _unsortedOperands = [this.M({}, "y", undefined, "q"), this.M(null, undefined, [], "y"), this.M(null, null, 12, "x"), this.M(null, 34, 35, {}), this.M(3, [], {}, {}), this.M([], [], "z", null), this.M(62, 18, "q", {}), this.M(74, undefined, 54, "x"), this.M("q", 38, {}, {})]; + let _operands = [_unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[4]]; + // LOAD_INT + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + // UNARY_BITNOT + var dst = _operands[2]; + regs[base + dst] = ~regs[base + _operands[3]]; + // UNARY_NOT + var dst = _operands[4]; + regs[base + dst] = !regs[base + _operands[5]]; + // EQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a === regs[base + _operands[8]]; +}; +VMPrototype[34] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(null, {}, 20, {}); + regs[base + dst] = !regs[base + this.M(null, undefined, [], {})]; +}; +VMPrototype[77] = function () { + var fp = this.y; + var regs = this.f; + // JUMP_3768 (specialized) + regs[fp + 0] = 3768; +}; +VMPrototype[50] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // obj, key, fn + var obj = regs[base + this.M(null, {}, [], 37)]; + var key = regs[base + this.M("x", "z", {}, {})]; + var getterFn = regs[base + this.M(49, "z", undefined, "y")]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); +}; +VMPrototype[3] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + var dst = this.M(45, undefined, null, undefined); + regs[base + dst] = regs[fp + 4].ah[this.M([], "k", "y", [])].d(undefined, 20, undefined, undefined); +}; +VMPrototype[4] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + var dst = this.M(undefined, "y", "x", []); + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[127] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_THIS,LOAD_CONST (macro) + // LOAD_THIS + var dst = this.M(null, null, "k", 82); + regs[base + dst] = regs[fp + 3]; + // LOAD_CONST + var dst = this.M(2, [], "z", undefined); + regs[base + dst] = this.g(); +}; +VMPrototype[114] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // JUMP_REG_87 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + 87]; +}; +VMPrototype[94] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // RETURN_16 (specialized) + var retVal = regs[base + 16]; + this.N(fp, "k", "y", null, undefined); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.z = fp; + this.y = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[12] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M([], "x", 6, 69); + var a = regs[base + this.M(null, {}, undefined, undefined)]; + regs[base + dst] = a - regs[base + this.M("z", 33, [], {})]; +}; +VMPrototype[92] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // GET_PROP_20_18_4 (specialized) + // dst = regs[obj][regs[key]] + var dst = 20; + var obj = regs[base + 18]; + var key = regs[base + 4]; + regs[base + dst] = obj[key]; +}; +VMPrototype[76] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_UNARY_BITNOT_2_7_6_0_8_1_9_3_5_4 (order: [2,7,6,0,8,1,9,3,5,4]) + let _unsortedOperands = [this.M(null, null, null, 12), this.M([], [], undefined, null), this.M([], null, {}, []), this.M(undefined, "z", null, "q"), this.M({}, 55, [], null), this.M({}, null, 72, 87), this.M({}, [], "x", null), this.M({}, null, null, "q"), this.M([], 98, {}, 26), this.M(undefined, null, 86, undefined)]; + let _operands = [_unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[6]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // MOD + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a % regs[base + _operands[4]]; + // SUB + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a - regs[base + _operands[7]]; + // UNARY_NEG + var dst = _operands[8]; + regs[base + dst] = -regs[base + _operands[9]]; +}; +VMPrototype[132] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this.M(51, [], undefined, {}); + var globalName = this.g(); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; + // LOAD_CONST + var dst = this.M(undefined, "k", "q", "k"); + regs[base + dst] = this.g(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.M(null, undefined, "x", "x"); + var obj = regs[base + this.M(null, [], null, "y")]; + var key = regs[base + this.M({}, 91, null, {})]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.M(9, undefined, 37, undefined); + regs[base + dst] = this.g(); +}; +VMPrototype[25] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M("z", [], 40, "x"); + var a = regs[base + this.M(null, null, 26, null)]; + regs[base + dst] = a >= regs[base + this.M(null, null, "z", [])]; +}; +VMPrototype[111] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // GET_PROP_85_3_84 (specialized) + // dst = regs[obj][regs[key]] + var dst = 85; + var obj = regs[base + 3]; + var key = regs[base + 84]; + regs[base + dst] = obj[key]; +}; +VMPrototype[97] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_UPVALUE_14_0 (specialized) + var dst = 14; + regs[base + dst] = regs[fp + 4].ah[0].d(8, "k", [], "x"); +}; +VMPrototype[42] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.M([], 36, undefined, undefined); + var callee = regs[base + this.M(15, {}, "z", undefined)]; + var argc = this.M("z", "y", {}, 98); + var args; + if (argc === 65535) { + args = regs[base + this.M([], null, null, undefined)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.M(98, "x", null, 60)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.j(args, [], 33, "z", closure, this.J, dst << 1, {}); + } else { + regs[base + dst] = callee.apply(null, args); + } +}; +VMPrototype[41] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // || short-circuit: if truthy, jump over RHS. + var src = this.M(null, 16, [], 51); + var target = this.M({}, [], null, null); + if (regs[base + src]) regs[fp + 0] = target; +}; +VMPrototype[57] = function () { + debugger; +}; +VMPrototype[18] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(83, {}, [], []); + var a = regs[base + this.M(null, null, "x", {})]; + regs[base + dst] = a ^ regs[base + this.M(27, null, undefined, null)]; +}; +VMPrototype[47] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.M("y", [], 30, "z"); + var startPc = this.M({}, "y", null, []); + var paramCount = this.M(null, undefined, null, null); + var regCount = this.M(28, undefined, null, undefined); + var uvCount = this.M(62, [], undefined, 99); + var hasRest = this.M([], [], 64, 99); // 1 if last param is a rest element + + // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.M([], "y", {}, null); + var uvIndex = this.M("z", 78, [], "k"); + uvDescs[i] = { + ac: isLocalRaw, + i: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + H: paramCount, + af: regCount, + ad: startPc, + E: hasRest + }; + var closure = new Closure(undefined, fn, 17); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.ac) { + closure.ah.push(this.Z(fp, undefined, uvd.i, {})); + } else { + closure.ah.push(regs[fp + 4].ah[uvd.i]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.J, undefined, "q", self.a, "q", self.ag, undefined).L(Array.prototype.slice.call(arguments), [], {}, undefined, this == null ? self.J : this, undefined, c); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + // unified prototype for new/instanceof + regs[base + dst] = shell; +}; +VMPrototype[90] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_INT_11_11 (specialized) + var dst = 11; + regs[base + dst] = 11; +}; +VMPrototype[58] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.M(28, {}, 39, [])]; +}; +VMPrototype[54] = function () { + var fp = this.y; + var regs = this.f; + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + k: this.M("z", "q", {}, {}), + e: this.M("q", undefined, [], null) + }); +}; +VMPrototype[20] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M("k", undefined, undefined, []); + var a = regs[base + this.M("q", 52, [], "q")]; + regs[base + dst] = a >> regs[base + this.M(undefined, null, null, {})]; +}; +VMPrototype[124] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // GET_PROP,LOAD_CONST (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.M(65, "y", 40, []); + var obj = regs[base + this.M(41, [], [], null)]; + var key = regs[base + this.M(undefined, 79, 0, "z")]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.M([], [], undefined, "q"); + regs[base + dst] = this.g(); +}; +VMPrototype[79] = function () { + var fp = this.y; + var regs = this.f; + // JUMP_6722 (specialized) + regs[fp + 0] = 6722; +}; +VMPrototype[116] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this.M(94, "k", "q", {}); + regs[base + dst] = this.M(90, null, undefined, []); + // LOAD_INT + var dst = this.M("y", undefined, [], undefined); + regs[base + dst] = this.M([], 39, [], 10); + // LOAD_INT + var dst = this.M(undefined, {}, undefined, undefined); + regs[base + dst] = this.M(undefined, [], undefined, "y"); +}; +VMPrototype[82] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST_12_0_2454948945 (specialized) + var dst = 12; + regs[base + dst] = this.g(0, undefined, undefined, [], 2454948945, 15); +}; +VMPrototype[150] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ALIAS_GET_PROP_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this.M([], {}, 9, 16), this.M({}, {}, [], []), this.M("x", null, {}, null)]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + // dst = regs[obj][regs[key]] + var dst = _operands[0]; + var obj = regs[base + _operands[1]]; + var key = regs[base + _operands[2]]; + regs[base + dst] = obj[key]; +}; +VMPrototype[24] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M({}, undefined, undefined, []); + var a = regs[base + this.M(undefined, 2, null, undefined)]; + regs[base + dst] = a <= regs[base + this.M([], [], 84, [])]; +}; +VMPrototype[30] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M({}, "y", "q", "q"); + var a = regs[base + this.M(undefined, null, [], undefined)]; + regs[base + dst] = a in regs[base + this.M("q", [], {}, 20)]; +}; +VMPrototype[106] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // GET_PROP_102_3_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 102; + var obj = regs[base + 3]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; +}; +VMPrototype[141] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.M(undefined, {}, undefined, 67); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(93, "x", [], {}); + regs[base + dst] = this.g(); + // LOAD_THIS + var dst = this.M(undefined, 96, [], undefined); + regs[base + dst] = regs[fp + 3]; + // LOAD_CONST + var dst = this.M("x", "z", "y", []); + regs[base + dst] = this.g(); +}; +VMPrototype[73] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_BOR_11_5_1_8_13_7_2_14_4_3_0_6_12_10_9 (order: [11,5,1,8,13,7,2,14,4,3,0,6,12,10,9]) + let _unsortedOperands = [this.M(41, [], null, undefined), this.M([], 79, {}, 80), this.M(undefined, null, "y", "q"), this.M({}, 63, [], []), this.M(null, "z", 76, {}), this.M(undefined, "x", {}, null), this.M("k", [], "y", 21), this.M([], undefined, null, undefined), this.M(null, null, [], "k"), this.M("y", undefined, {}, undefined), this.M(null, [], [], undefined), this.M(undefined, 82, {}, "x"), this.M(undefined, {}, undefined, "z"), this.M(undefined, undefined, "z", 56), this.M(null, {}, 24, undefined)]; + let _operands = [_unsortedOperands[10], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[14], _unsortedOperands[13], _unsortedOperands[0], _unsortedOperands[12], _unsortedOperands[4], _unsortedOperands[7]]; + // LOAD_GLOBAL + var dst = _operands[0]; + var globalName = this.g(_operands[1], "z", "q", undefined, _operands[2], []); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; + // BOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a | regs[base + _operands[5]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[6]; + var name = this.g(_operands[7], undefined, undefined, {}, _operands[8], []); + var val = Object.prototype.hasOwnProperty.call(this.J, name) ? this.J[name] : undefined; + regs[base + dst] = typeof val; + // LOAD_GLOBAL + var dst = _operands[9]; + var globalName = this.g(_operands[10], 70, null, undefined, _operands[11], "k"); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; + // LTE + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a <= regs[base + _operands[14]]; +}; +VMPrototype[78] = function () { + var fp = this.y; + var regs = this.f; + // JUMP_10434 (specialized) + regs[fp + 0] = 10434; +}; +VMPrototype[21] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(26, null, undefined, {}); + var a = regs[base + this.M(null, 12, "y", null)]; + regs[base + dst] = a >>> regs[base + this.M("k", [], undefined, "z")]; +}; +VMPrototype[123] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this.M(81, {}, [], "z"); + regs[base + dst] = this.g(); + // RETURN + var retVal = regs[base + this.M(39, {}, null, undefined)]; + this.N(fp, undefined, 31, undefined, "q"); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.z = fp; + this.y = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[145] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ALIAS_LOAD_CONST_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this.M("y", null, {}, undefined), this.M(undefined, {}, 86, {}), this.M([], [], "y", null)]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = this.g(_operands[1], 75, undefined, [], _operands[2], {}); +}; +VMPrototype[35] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(41, 39, [], []); + regs[base + dst] = ~regs[base + this.M(1, {}, {}, "q")]; +}; +VMPrototype[2] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M({}, {}, {}, 19); + var globalName = this.g(); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; +}; +VMPrototype[105] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // THROW_5 (specialized) + throw regs[base + 5]; +}; +VMPrototype[68] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_ADD_7_6_5_9_4_3_1_10_8_0_2 (order: [7,6,5,9,4,3,1,10,8,0,2]) + let _unsortedOperands = [this.M([], [], undefined, "k"), this.M("y", {}, null, undefined), this.M(undefined, [], undefined, {}), this.M(66, 56, "q", undefined), this.M(undefined, [], [], "x"), this.M({}, {}, null, "k"), this.M("k", "q", 13, "k"), this.M({}, [], 97, null), this.M("q", undefined, null, {}), this.M(null, 81, [], null), this.M([], 51, 69, null)]; + let _operands = [_unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[7]]; + // TYPEOF + var dst = _operands[0]; + regs[base + dst] = typeof regs[base + _operands[1]]; + // LOOSE_EQ + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a == regs[base + _operands[4]]; + // LOOSE_EQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a == regs[base + _operands[7]]; + // ADD + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a + regs[base + _operands[10]]; +}; +VMPrototype[144] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,CALL (macro) + // LOAD_CONST + var dst = this.M(99, undefined, 3, null); + regs[base + dst] = this.g(); + // CALL + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.M([], {}, null, {}); + var callee = regs[base + this.M({}, undefined, {}, null)]; + var argc = this.M([], {}, {}, undefined); + var args; + if (argc === 65535) { + args = regs[base + this.M({}, 0, null, {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.M(92, "k", {}, {})]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.j(args, {}, [], [], closure, this.J, dst << 1, []); + } else { + regs[base + dst] = callee.apply(null, args); + } +}; +VMPrototype[81] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_INT_18_8 (specialized) + var dst = 18; + regs[base + dst] = 8; +}; +VMPrototype[8] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // dst = regs[obj][regs[key]] + var dst = this.M(undefined, {}, 13, {}); + var obj = regs[base + this.M({}, {}, "y", [])]; + var key = regs[base + this.M(89, [], [], "y")]; + regs[base + dst] = obj[key]; +}; +VMPrototype[53] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.M({}, [], null, "z"); + var iter = regs[base + this.M("x", 81, null, null)]; + var exitTarget = this.M([], {}, null, undefined); + if (iter.W >= iter.V.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.V[iter.W++]; + } +}; +VMPrototype[129] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this.M(null, "q", null, null); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(null, undefined, undefined, undefined); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(null, [], 27, undefined); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(undefined, undefined, {}, 14); + regs[base + dst] = this.g(); + // LOAD_THIS + var dst = this.M(81, {}, 95, 42); + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[22] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M({}, {}, {}, 40); + var a = regs[base + this.M(undefined, null, "x", {})]; + regs[base + dst] = a < regs[base + this.M(59, null, "y", [])]; +}; +VMPrototype[86] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[110] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_THIS_15 (specialized) + var dst = 15; + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[28] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M([], "x", [], {}); + var a = regs[base + this.M([], {}, "k", [])]; + regs[base + dst] = a == regs[base + this.M([], "z", "q", null)]; +}; +VMPrototype[121] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this.M([], 26, null, "z"); + regs[base + dst] = this.g(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.M(32, undefined, {}, undefined); + var obj = regs[base + this.M(null, {}, undefined, 23)]; + var key = regs[base + this.M("x", null, {}, [])]; + regs[base + dst] = obj[key]; +}; +VMPrototype[131] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this.M("y", null, {}, []); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M({}, {}, [], {}); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(null, 19, null, undefined); + regs[base + dst] = this.g(); + // LOAD_THIS + var dst = this.M({}, 27, null, null); + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[59] = function () { + var fp = this.y; + var regs = this.f; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + m: this.M(null, undefined, [], []), + T: this.M(91, {}, 81, undefined), + s: this.M(99, [], [], "q"), + c: this.M("z", {}, {}, undefined) + }); +}; +VMPrototype[91] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_GLOBAL_18_6_313453912 (specialized) + var dst = 18; + var globalName = this.g(6, [], null, "x", 313453912, 73); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; +}; +VMPrototype[66] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // ANTI_SUB_3_8_11_9_2_7_13_10_4_6_12_0_1_5 (order: [3,8,11,9,2,7,13,10,4,6,12,0,1,5]) + let _unsortedOperands = [this.M({}, 53, 80, {}), this.M([], null, {}, {}), this.M("q", [], null, "q"), this.M(35, [], null, undefined), this.M(undefined, [], [], "k"), this.M({}, undefined, [], {}), this.M(undefined, null, null, "q"), this.M(null, [], 49, "z"), this.M("k", [], 53, null), this.M(67, null, 75, "y"), this.M([], [], 31, undefined), this.M(null, {}, 87, []), this.M(undefined, "y", null, 24), this.M(undefined, undefined, null, null)]; + let _operands = [_unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[13], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[10], _unsortedOperands[6]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // LOOSE_NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a != regs[base + _operands[5]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[6]; + var target = _operands[7]; + if (regs[base + src]) regs[fp + 0] = target; + // ADD + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a + regs[base + _operands[10]]; + // SUB + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a - regs[base + _operands[13]]; +}; +VMPrototype[115] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this.M(null, 23, 8, {}); + regs[base + dst] = this.M({}, "y", "y", 72); + // LOAD_INT + var dst = this.M(null, undefined, null, undefined); + regs[base + dst] = this.M({}, "x", "k", undefined); +}; +VMPrototype[107] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // THROW_102 (specialized) + throw regs[base + 102]; +}; +VMPrototype[9] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.M(77, [], "q", "x")]; + var key = regs[base + this.M(null, [], undefined, 30)]; + var val = regs[base + this.M({}, undefined, [], [])]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[11] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M({}, undefined, {}, []); + var a = regs[base + this.M("y", 30, "y", "x")]; + regs[base + dst] = a + regs[base + this.M(undefined, undefined, null, null)]; +}; +VMPrototype[88] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_INT_9_3115171626 (specialized) + var dst = 9; + regs[base + dst] = 3115171626; +}; +VMPrototype[61] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // ANTI_MOVE_6_0_9_2_5_3_1_7_4_8 (order: [6,0,9,2,5,3,1,7,4,8]) + let _unsortedOperands = [this.M(null, [], {}, undefined), this.M({}, [], undefined, []), this.M(undefined, "k", undefined, {}), this.M(32, 5, "z", {}), this.M(null, null, {}, undefined), this.M(undefined, "x", 92, undefined), this.M({}, null, undefined, "z"), this.M("q", "k", null, {}), this.M(undefined, undefined, 74, {}), this.M(undefined, null, "y", undefined)]; + let _operands = [_unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[9], _unsortedOperands[2]]; + // LOAD_GLOBAL + var dst = _operands[0]; + var globalName = this.g(_operands[1], undefined, 60, {}, _operands[2], []); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // MOVE + var dst = _operands[6]; + regs[base + dst] = regs[base + _operands[7]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[8]; + var target = _operands[9]; + if (regs[base + src]) regs[fp + 0] = target; +}; +VMPrototype[84] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST_15_2_672545599 (specialized) + var dst = 15; + regs[base + dst] = this.g(2, null, [], [], 672545599, []); +}; +VMPrototype[5] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M([], [], null, {}); + regs[base + dst] = regs[base + this.M("q", "q", {}, undefined)]; +}; +VMPrototype[108] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // GET_PROP_92_92_95 (specialized) + // dst = regs[obj][regs[key]] + var dst = 92; + var obj = regs[base + 92]; + var key = regs[base + 95]; + regs[base + dst] = obj[key]; +}; +VMPrototype[75] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_USHR_3_10_6_5_8_1_4_0_2_7_9 (order: [3,10,6,5,8,1,4,0,2,7,9]) + let _unsortedOperands = [this.M(null, null, [], 93), this.M(undefined, 22, undefined, []), this.M("z", "k", "k", undefined), this.M("z", undefined, [], {}), this.M("k", "x", null, 72), this.M(null, null, 37, null), this.M("q", 22, "y", []), this.M("k", "z", 9, {}), this.M(null, [], {}, undefined), this.M("q", {}, 32, "x"), this.M(1, undefined, [], undefined)]; + let _operands = [_unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[10], _unsortedOperands[1]]; + // GTE + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >= regs[base + _operands[2]]; + // EQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a === regs[base + _operands[5]]; + // USHR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >>> regs[base + _operands[8]]; + // LOAD_INT + var dst = _operands[9]; + regs[base + dst] = _operands[10]; +}; +VMPrototype[16] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M({}, 34, "y", null); + var a = regs[base + this.M(37, "y", [], 15)]; + regs[base + dst] = a & regs[base + this.M(14, [], 82, 63)]; +}; +VMPrototype[117] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.M(undefined, null, null, []); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M("z", "k", 65, null); + regs[base + dst] = this.g(); +}; +VMPrototype[135] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.M(null, 9, "y", "y"); + var obj = regs[base + this.M(undefined, [], "z", undefined)]; + var key = regs[base + this.M(undefined, "z", 15, {})]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.M(69, [], {}, []); + regs[base + dst] = this.g(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.M({}, {}, 95, undefined); + var receiver = regs[base + this.M({}, 2, "k", 97)]; + var callee = regs[base + this.M(56, undefined, {}, [])]; + var argc = this.M("y", "z", "z", []); + var args; + if (argc === 65535) { + args = regs[base + this.M("x", "z", null, undefined)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.M("k", [], "y", 97)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.j(args, {}, null, 20, closure, receiver, dst << 1, undefined); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[128] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.M({}, undefined, {}, []); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M({}, [], "z", "z"); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M({}, [], undefined, undefined); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M("k", 91, null, null); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(null, undefined, undefined, "q"); + regs[base + dst] = this.g(); +}; +VMPrototype[37] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(undefined, {}, null, null); + this.M([], null, undefined, undefined); // consumes argument (intended) + // consumes argument (intended) + regs[base + dst] = undefined; +}; +VMPrototype[39] = function () { + var fp = this.y; + var regs = this.f; + regs[fp + 0] = this.M([], null, 52, {}); +}; +VMPrototype[87] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_GLOBAL_16_6_313453912 (specialized) + var dst = 16; + var globalName = this.g(6, [], null, null, 313453912, {}); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; +}; +VMPrototype[80] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this.N(fp, [], [], null, "q"); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.z = fp; + this.y = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[33] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(undefined, [], {}, 43); + regs[base + dst] = +regs[base + this.M([], [], 45, 14)]; +}; +VMPrototype[109] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // JUMP_REG_92 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + 92]; +}; +VMPrototype[60] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // Math.pow instead of `**` + var dst = this.M("y", 61, 42, null); + var a = regs[base + this.M({}, [], {}, undefined)]; + regs[base + dst] = Math.pow(a, regs[base + this.M(undefined, undefined, {}, 28)]); +}; +VMPrototype[46] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + throw regs[base + this.M({}, {}, {}, undefined)]; +}; +VMPrototype[74] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_BXOR_7_2_6_5_1_8_3_0_10_9_12_11_4 (order: [7,2,6,5,1,8,3,0,10,9,12,11,4]) + let _unsortedOperands = [this.M("q", 92, "k", []), this.M(34, "x", "x", undefined), this.M("k", undefined, "k", "q"), this.M({}, null, 14, undefined), this.M("k", "x", "x", "y"), this.M("x", [], {}, {}), this.M(84, "x", "z", []), this.M([], "q", {}, undefined), this.M(null, "q", undefined, []), this.M("k", null, [], []), this.M({}, 47, "z", null), this.M(undefined, null, 51, []), this.M({}, 33, [], undefined)]; + let _operands = [_unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[12], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[11], _unsortedOperands[10]]; + // LTE + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a <= regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // BXOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a ^ regs[base + _operands[8]]; + // MOVE + var dst = _operands[9]; + regs[base + dst] = regs[base + _operands[10]]; + // MOVE + var dst = _operands[11]; + regs[base + dst] = regs[base + _operands[12]]; +}; +VMPrototype[48] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // dst, count, [elemReg...] + var dst = this.M(undefined, 16, null, null); + var count = this.M(null, null, undefined, 74); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.M("x", {}, undefined, "k")]; + regs[base + dst] = elems; +}; +VMPrototype[38] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.M(null, [], 55, []); + var name = this.g(); + var val = Object.prototype.hasOwnProperty.call(this.J, name) ? this.J[name] : undefined; + regs[base + dst] = typeof val; +}; +VMPrototype[36] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(85, 87, {}, "x"); + regs[base + dst] = typeof regs[base + this.M("z", 15, undefined, 47)]; +}; +VMPrototype[93] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // SET_PROP_16_3_18 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 16]; + var key = regs[base + 3]; + var val = regs[base + 18]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[1] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M([], 12, "q", null); + regs[base + dst] = this.M("z", "x", {}, null); +}; +VMPrototype[49] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.M(undefined, "y", {}, {}); + var pairCount = this.M({}, null, 80, "y"); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.M(7, [], {}, null)]; + var val = regs[base + this.M(null, 57, [], 91)]; + o[key] = val; + } + regs[base + dst] = o; +}; +VMPrototype[7] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + var uvIdx = this.M("q", null, undefined, {}); + regs[fp + 4].ah[uvIdx].U(regs[base + this.M(73, [], [], {})], 70, "z"); +}; +VMPrototype[29] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(null, 21, undefined, {}); + var a = regs[base + this.M(18, 34, {}, undefined)]; + regs[base + dst] = a != regs[base + this.M(null, undefined, "k", "y")]; +}; +VMPrototype[19] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M("k", "x", 47, "k"); + var a = regs[base + this.M(undefined, null, null, 67)]; + regs[base + dst] = a << regs[base + this.M({}, "q", null, "k")]; +}; +VMPrototype[146] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ALIAS_LOAD_INT_1_0 (order: [1,0]) + let _unsortedOperands = [this.M(undefined, 26, "x", {}), this.M(undefined, null, undefined, undefined)]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = _operands[1]; +}; +VMPrototype[103] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // JUMP_REG_4 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + 4]; +}; +VMPrototype[147] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this.M([], null, "y", {})]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this.N(fp, null, [], undefined, 18); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.z = fp; + this.y = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[96] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_GLOBAL_7_22_3356184819 (specialized) + var dst = 7; + var globalName = this.g(22, 62, {}, undefined, 3356184819, 53); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; +}; +VMPrototype[27] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(undefined, [], null, 37); + var a = regs[base + this.M("y", [], null, undefined)]; + regs[base + dst] = a !== regs[base + this.M(33, "y", "k", "k")]; +}; +VMPrototype[40] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + var src = this.M(null, null, {}, []); + var target = this.M("q", [], {}, 78); + if (!regs[base + src]) regs[fp + 0] = target; +}; +VMPrototype[133] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this.M(null, undefined, undefined, null); + regs[base + dst] = this.g(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.M(23, undefined, null, "z"); + var obj = regs[base + this.M(null, [], "y", undefined)]; + var key = regs[base + this.M(77, undefined, null, 41)]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.M(undefined, "q", {}, {}); + regs[base + dst] = this.g(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.M({}, 64, [], 9); + var receiver = regs[base + this.M(undefined, [], null, "y")]; + var callee = regs[base + this.M(undefined, "z", "k", "z")]; + var argc = this.M({}, [], {}, null); + var args; + if (argc === 65535) { + args = regs[base + this.M("q", 42, "z", null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.M({}, [], {}, 30)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.j(args, null, {}, 79, closure, receiver, dst << 1, null); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[130] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_GLOBAL + var dst = this.M(null, null, {}, {}); + var globalName = this.g(); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; + // LOAD_CONST + var dst = this.M({}, "q", {}, {}); + regs[base + dst] = this.g(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.M(84, "q", {}, 98); + var obj = regs[base + this.M(29, null, 24, null)]; + var key = regs[base + this.M(11, {}, {}, "q")]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.M(96, 6, 42, {}); + regs[base + dst] = this.g(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.M([], [], null, 48); + var receiver = regs[base + this.M({}, 56, undefined, undefined)]; + var callee = regs[base + this.M(undefined, null, {}, {})]; + var argc = this.M("q", undefined, "q", undefined); + var args; + if (argc === 65535) { + args = regs[base + this.M([], null, "k", 40)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.M(null, 84, [], "k")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.j(args, {}, 47, 17, closure, receiver, dst << 1, "z"); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[6] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // globals[globalName] = regs[src] + this.J[this.g()] = regs[base + this.M("x", [], 28, {})]; +}; +VMPrototype[99] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_GLOBAL_11_6_313453912 (specialized) + var dst = 11; + var globalName = this.g(6, undefined, null, {}, 313453912, undefined); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; +}; +VMPrototype[101] = function () { + var fp = this.y; + var regs = this.f; + // TRY_SETUP_3735_3 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + k: 3735, + e: 3 + }); +}; +VMPrototype[118] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.M({}, [], {}, undefined); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M([], {}, 32, null); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(undefined, undefined, undefined, null); + regs[base + dst] = this.g(); +}; +VMPrototype[72] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // ANTI_SHL_6_9_13_12_7_10_5_4_8_11_0_2_3_1 (order: [6,9,13,12,7,10,5,4,8,11,0,2,3,1]) + let _unsortedOperands = [this.M(undefined, [], "y", {}), this.M({}, {}, {}, "q"), this.M([], "k", 23, null), this.M({}, 53, null, []), this.M("q", undefined, null, undefined), this.M("z", "y", undefined, null), this.M([], [], undefined, []), this.M([], [], 61, "k"), this.M(96, "x", undefined, "q"), this.M(null, [], "k", undefined), this.M([], 85, 74, null), this.M("z", "y", 52, {}), this.M({}, 8, 69, undefined), this.M(null, "k", null, undefined)]; + let _operands = [_unsortedOperands[10], _unsortedOperands[13], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[3], _unsortedOperands[2]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[6]; + var target = _operands[7]; + if (regs[base + src]) regs[fp + 0] = target; + // LTE + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a <= regs[base + _operands[10]]; + // MUL + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a * regs[base + _operands[13]]; +}; +VMPrototype[104] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // RETURN_102 (specialized) + var retVal = regs[base + 102]; + this.N(fp, "x", null, undefined, undefined); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.z = fp; + this.y = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[31] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.M(null, 9, 72, 9); + var obj = regs[base + this.M(null, null, 47, 1)]; + regs[base + dst] = obj instanceof regs[base + this.M("x", {}, [], "k")]; +}; +VMPrototype[64] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_UNARY_NOT_4_3_1_7_2_9_8_6_5_0_10 (order: [4,3,1,7,2,9,8,6,5,0,10]) + let _unsortedOperands = [this.M({}, {}, "x", []), this.M({}, [], null, null), this.M([], null, 48, "x"), this.M({}, null, "k", 34), this.M(38, 1, {}, undefined), this.M(null, undefined, null, []), this.M({}, null, undefined, {}), this.M(61, null, undefined, "x"), this.M(undefined, undefined, 83, []), this.M(undefined, "x", [], undefined), this.M(null, [], {}, 9)]; + let _operands = [_unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[10]]; + // MOD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a % regs[base + _operands[2]]; + // UNARY_NOT + var dst = _operands[3]; + regs[base + dst] = !regs[base + _operands[4]]; + // EQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a === regs[base + _operands[7]]; + // MOD + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a % regs[base + _operands[10]]; +}; +VMPrototype[14] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M([], {}, "y", undefined); + var a = regs[base + this.M(31, "x", [], 88)]; + regs[base + dst] = a / regs[base + this.M(17, undefined, null, {})]; +}; +VMPrototype[113] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // JUMP_REG_73 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + 73]; +}; +VMPrototype[139] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_INT,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this.M(undefined, null, [], {}); + regs[base + dst] = this.M(null, "k", null, "q"); + // LOAD_INT + var dst = this.M(36, [], undefined, []); + regs[base + dst] = this.M({}, {}, 20, {}); + // LOAD_INT + var dst = this.M(null, "z", 6, null); + regs[base + dst] = this.M("k", 19, [], {}); + // LOAD_INT + var dst = this.M("k", 56, 48, undefined); + regs[base + dst] = this.M([], undefined, [], undefined); +}; +VMPrototype[120] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this.M(null, undefined, {}, undefined); + regs[base + dst] = this.g(); + // LOAD_INT + var dst = this.M(undefined, 41, undefined, "q"); + regs[base + dst] = this.M({}, null, undefined, {}); +}; +VMPrototype[71] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_LT_8_3_1_2_9_7_10_0_6_11_4_5 (order: [8,3,1,2,9,7,10,0,6,11,4,5]) + let _unsortedOperands = [this.M({}, [], undefined, null), this.M(undefined, [], 15, null), this.M(undefined, {}, [], "x"), this.M({}, {}, {}, "q"), this.M(36, 70, undefined, []), this.M([], undefined, [], {}), this.M(null, "y", undefined, undefined), this.M(null, 78, [], []), this.M(31, [], "q", null), this.M(undefined, null, 53, {}), this.M("x", "q", undefined, {}), this.M("z", [], null, {})]; + let _operands = [_unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[11], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[9]]; + // SHL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a << regs[base + _operands[2]]; + // SUB + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a - regs[base + _operands[5]]; + // SHR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >> regs[base + _operands[8]]; + // LT + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a < regs[base + _operands[11]]; +}; +VMPrototype[70] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_BAND_6_8_11_10_2_3_1_0_4_5_7_9 (order: [6,8,11,10,2,3,1,0,4,5,7,9]) + let _unsortedOperands = [this.M(34, "y", [], 51), this.M(null, 24, [], null), this.M(undefined, 93, {}, []), this.M(69, [], [], undefined), this.M("q", "x", [], null), this.M(null, undefined, "q", undefined), this.M(undefined, {}, null, []), this.M(undefined, "k", null, null), this.M(null, 97, null, []), this.M(null, [], [], undefined), this.M(null, "x", undefined, null), this.M("z", [], null, "k")]; + let _operands = [_unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[0], _unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[11], _unsortedOperands[3], _unsortedOperands[2]]; + // LOOSE_NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a != regs[base + _operands[2]]; + // EQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a === regs[base + _operands[5]]; + // BAND + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a & regs[base + _operands[8]]; + // GTE + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a >= regs[base + _operands[11]]; +}; +VMPrototype[134] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this.M([], [], 65, []); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(85, null, [], {}); + regs[base + dst] = this.g(); + // LOAD_THIS + var dst = this.M(undefined, [], {}, "y"); + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[98] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[143] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.M([], 47, [], null); + regs[base + dst] = this.g(); + // LOAD_THIS + var dst = this.M(47, "z", 59, {}); + regs[base + dst] = regs[fp + 3]; + // LOAD_CONST + var dst = this.M(undefined, {}, "q", []); + regs[base + dst] = this.g(); +}; +VMPrototype[26] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(null, 50, {}, {}); + var a = regs[base + this.M(41, undefined, null, {})]; + regs[base + dst] = a === regs[base + this.M({}, undefined, null, "y")]; +}; +VMPrototype[122] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.M(null, [], {}, {}); + regs[base + dst] = this.g(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.M([], {}, "y", {}); + var obj = regs[base + this.M(undefined, null, "y", null)]; + var key = regs[base + this.M(undefined, [], {}, null)]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.M(13, 22, {}, {}); + regs[base + dst] = this.g(); +}; +VMPrototype[119] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.M(undefined, null, {}, 20); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(undefined, {}, {}, 55); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M([], [], 0, {}); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M({}, [], [], undefined); + regs[base + dst] = this.g(); +}; +VMPrototype[0] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(null, undefined, "y", null); + regs[base + dst] = this.g(); +}; +VMPrototype[102] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // SET_PROP_11_12_13 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 11]; + var key = regs[base + 12]; + var val = regs[base + 13]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[17] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(undefined, 49, {}, []); + var a = regs[base + this.M(null, null, "q", {})]; + regs[base + dst] = a | regs[base + this.M({}, null, [], 76)]; +}; +VMPrototype[138] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.M(undefined, null, null, null); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(undefined, 75, null, undefined); + regs[base + dst] = this.g(); + // LOAD_CONST + var dst = this.M(null, undefined, "q", "q"); + regs[base + dst] = this.g(); + // LOAD_THIS + var dst = this.M(null, "z", undefined, []); + regs[base + dst] = regs[fp + 3]; + // LOAD_CONST + var dst = this.M([], null, 92, null); + regs[base + dst] = this.g(); +}; +VMPrototype[89] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_INT_10_199 (specialized) + var dst = 10; + regs[base + dst] = 199; +}; +VMPrototype[63] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_NEQ_8_14_5_6_13_3_1_10_0_2_7_9_4_11_12 (order: [8,14,5,6,13,3,1,10,0,2,7,9,4,11,12]) + let _unsortedOperands = [this.M(null, "z", 82, 78), this.M({}, [], null, undefined), this.M({}, [], [], {}), this.M("x", 33, [], "k"), this.M([], {}, "k", undefined), this.M(null, "k", 62, 37), this.M(88, undefined, undefined, null), this.M(null, [], undefined, []), this.M(undefined, undefined, {}, undefined), this.M({}, 15, 36, 25), this.M({}, undefined, "q", 2), this.M(83, {}, 33, 6), this.M(null, undefined, null, "k"), this.M([], {}, undefined, null), this.M(87, [], [], 49)]; + let _operands = [_unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[12], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[7], _unsortedOperands[13], _unsortedOperands[14], _unsortedOperands[4], _unsortedOperands[1]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // LOAD_CONST + var dst = _operands[3]; + regs[base + dst] = this.g(_operands[4], [], 84, null, _operands[5], 21); + // EQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a === regs[base + _operands[8]]; + // BAND + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a & regs[base + _operands[11]]; + // EQ + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a === regs[base + _operands[14]]; +}; +VMPrototype[140] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,GET_PROP,LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this.M(null, {}, "y", {}); + regs[base + dst] = this.g(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.M([], {}, null, undefined); + var obj = regs[base + this.M(null, "k", null, undefined)]; + var key = regs[base + this.M("y", "z", [], {})]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.M(undefined, null, 75, "y"); + regs[base + dst] = this.g(); + // LOAD_INT + var dst = this.M("q", undefined, 1, undefined); + regs[base + dst] = this.M(null, undefined, null, 22); +}; +VMPrototype[10] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.M(29, {}, {}, "x"); + var obj = regs[base + this.M(26, {}, {}, null)]; + var key = regs[base + this.M(undefined, undefined, 51, 10)]; + regs[base + dst] = delete obj[key]; +}; +VMPrototype[13] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(7, {}, [], undefined); + var a = regs[base + this.M("q", 90, undefined, undefined)]; + regs[base + dst] = a * regs[base + this.M(null, null, "z", {})]; +}; +VMPrototype[44] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.M("z", undefined, null, "y"); + var callee = regs[base + this.M(57, undefined, null, "z")]; + var argc = this.M(null, null, null, {}); + var args; + if (argc === 65535) { + args = regs[base + this.M("q", {}, {}, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.M(null, null, undefined, null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.j(args, undefined, {}, null, closure, Object.create(closure.prototype || null), dst << 1 | 1, 77); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } +}; +VMPrototype[83] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST_14_1_3566713139 (specialized) + var dst = 14; + regs[base + dst] = this.g(1, null, 35, {}, 3566713139, undefined); +}; +VMPrototype[137] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this.M("x", 59, 28, null); + regs[base + dst] = this.g(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.M(undefined, {}, 47, 34); + var receiver = regs[base + this.M({}, undefined, {}, 57)]; + var callee = regs[base + this.M("x", 83, {}, [])]; + var argc = this.M("y", null, null, null); + var args; + if (argc === 65535) { + args = regs[base + this.M([], 29, 44, [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.M(undefined, 81, {}, [])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.j(args, 88, null, [], closure, receiver, dst << 1, []); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[52] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.M("y", [], null, "z"); + var obj = regs[base + this.M(undefined, 24, undefined, 38)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + V: keys, + W: 0 + }; +}; +VMPrototype[45] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + var retVal = regs[base + this.M("y", undefined, 50, null)]; + this.N(fp, {}, undefined, "z", null); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.z = fp; + this.y = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[142] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // GET_PROP,LOAD_CONST,LOAD_INT (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.M([], null, [], null); + var obj = regs[base + this.M("y", [], {}, [])]; + var key = regs[base + this.M(undefined, null, 49, undefined)]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.M(null, undefined, undefined, undefined); + regs[base + dst] = this.g(); + // LOAD_INT + var dst = this.M(92, [], null, "q"); + regs[base + dst] = this.M(null, "z", "x", 14); +}; +VMPrototype[43] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.M(null, undefined, 44, "x"); + var receiver = regs[base + this.M(undefined, {}, {}, {})]; + var callee = regs[base + this.M({}, {}, [], "x")]; + var argc = this.M("k", 25, "k", []); + var args; + if (argc === 65535) { + args = regs[base + this.M([], [], [], {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.M("k", "k", undefined, null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.j(args, [], "x", 21, closure, receiver, dst << 1, null); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[23] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M("z", undefined, {}, "k"); + var a = regs[base + this.M("z", null, [], 85)]; + regs[base + dst] = a > regs[base + this.M(undefined, 74, {}, undefined)]; +}; +VMPrototype[100] = function () { + var fp = this.y; + var regs = this.f; + // FINALLY_SETUP_2975_4_5_3361 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + m: 2975, + T: 4, + s: 5, + c: 3361 + }); +}; +VMPrototype[56] = function () { + // destPc, sliceStart, sliceEnd, key + var destPc = this.M(null, 95, {}, undefined); + var sliceStart = this.M(undefined, null, null, "x"); + var sliceEnd = this.M(0, undefined, undefined, {}); + var pk = this.M([], {}, {}, {}) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.ag[destPc + (pi - sliceStart)] = (this.ag[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } +}; +VMPrototype[32] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(48, null, {}, []); + regs[base + dst] = -regs[base + this.M(65, 86, "x", undefined)]; +}; +VMPrototype[112] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // RETURN_4 (specialized) + var retVal = regs[base + 4]; + this.N(fp, 0, 99, undefined, "k"); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.z = fp; + this.y = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[148] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ALIAS_LOAD_GLOBAL_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this.M([], "k", "q", undefined), this.M("x", [], {}, undefined), this.M({}, {}, "q", undefined)]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + var globalName = this.g(_operands[1], undefined, null, {}, _operands[2], undefined); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; +}; +VMPrototype[65] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_UNARY_POS_2_6_0_3_4_1_7_5 (order: [2,6,0,3,4,1,7,5]) + let _unsortedOperands = [this.M([], null, {}, null), this.M(null, 88, undefined, []), this.M("z", null, {}, "q"), this.M(null, 20, undefined, 64), this.M(undefined, [], {}, []), this.M({}, null, null, undefined), this.M({}, [], {}, null), this.M([], undefined, 20, [])]; + let _operands = [_unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[6]]; + // UNARY_POS + var dst = _operands[0]; + regs[base + dst] = +regs[base + _operands[1]]; + // BAND + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a & regs[base + _operands[4]]; + // LOAD_GLOBAL + var dst = _operands[5]; + var globalName = this.g(_operands[6], null, {}, undefined, _operands[7], null); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; +}; +VMPrototype[85] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_CONST_13_3_2390270473 (specialized) + var dst = 13; + regs[base + dst] = this.g(3, 26, [], {}, 2390270473, {}); +}; +VMPrototype[55] = function () { + var fp = this.y; + var regs = this.f; + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); +}; +VMPrototype[15] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + var dst = this.M(undefined, {}, null, undefined); + var a = regs[base + this.M(15, "k", undefined, null)]; + regs[base + dst] = a % regs[base + this.M("k", [], undefined, [])]; +}; +VMPrototype[62] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_UNARY_NEG_6_7_5_1_4_3_9_8_2_0_10 (order: [6,7,5,1,4,3,9,8,2,0,10]) + let _unsortedOperands = [this.M("q", "y", "q", undefined), this.M("y", "y", "q", "y"), this.M(17, "k", [], {}), this.M(null, {}, null, []), this.M(null, "k", [], null), this.M(null, null, undefined, "k"), this.M(51, {}, undefined, []), this.M([], null, [], undefined), this.M({}, "x", [], "y"), this.M({}, [], {}, undefined), this.M(undefined, [], null, [])]; + let _operands = [_unsortedOperands[9], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[10]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this.g(_operands[1], undefined, {}, 79, _operands[2], []); + var val = Object.prototype.hasOwnProperty.call(this.J, name) ? this.J[name] : undefined; + regs[base + dst] = typeof val; + // LOOSE_EQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a == regs[base + _operands[5]]; + // UNARY_NEG + var dst = _operands[6]; + regs[base + dst] = -regs[base + _operands[7]]; + // LT + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a < regs[base + _operands[10]]; +}; +VMPrototype[149] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this.M("y", "z", null, "x")]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[67] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // ANTI_MUL_6_7_3_4_8_2_1_5_0 (order: [6,7,3,4,8,2,1,5,0]) + let _unsortedOperands = [this.M([], [], 34, null), this.M(43, undefined, null, undefined), this.M(null, 2, "k", undefined), this.M(null, 38, 55, 42), this.M(68, "z", 36, 92), this.M(null, null, "x", null), this.M(null, undefined, 7, "q"), this.M("k", {}, [], undefined), this.M([], undefined, [], [])]; + let _operands = [_unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[4]]; + // SUB + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a - regs[base + _operands[2]]; + // MUL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a * regs[base + _operands[5]]; + // LOAD_CONST + var dst = _operands[6]; + regs[base + dst] = this.g(_operands[7], null, [], null, _operands[8], null); +}; +VMPrototype[126] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this.M(null, undefined, undefined, "y"); + var globalName = this.g(); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; + // LOAD_CONST + var dst = this.M(91, [], 37, {}); + regs[base + dst] = this.g(); +}; +VMPrototype[95] = function () { + var fp = this.y; + var regs = this.f; + var base = regs[fp + 7]; + // LOAD_THIS_19 (specialized) + var dst = 19; + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[51] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // obj, key, fn + var obj = regs[base + this.M({}, {}, null, 90)]; + var key = regs[base + this.M(null, "z", {}, "y")]; + var setterFn = regs[base + this.M(undefined, [], "k", [])]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); +}; +VMPrototype[125] = function () { + var regs = this.f; + var base = regs[this.y + 7]; + // LOAD_GLOBAL,LOAD_CONST,GET_PROP (macro) + // LOAD_GLOBAL + var dst = this.M([], [], {}, {}); + var globalName = this.g(); + if (!(globalName in this.J)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.J[globalName]; + // LOAD_CONST + var dst = this.M(null, null, undefined, "y"); + regs[base + dst] = this.g(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.M(null, "z", undefined, "y"); + var obj = regs[base + this.M("x", undefined, undefined, [])]; + var key = regs[base + this.M(88, "x", 98, undefined)]; + regs[base + dst] = obj[key]; +}; +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(globals, undefined, null, CONSTANTS, [], decodeBytecode(BYTECODE), 47); +vm.L(null // no arguments object / parameter setup for the root frame +, "q", null, "z", undefined, 99, new Closure(0, { + H: 0, + af: 21, + ad: 0 +}, 13)); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/qualification.json new file mode 100644 index 00000000..f28abc94 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/qualification.json @@ -0,0 +1,82 @@ +{ + "id": "exceptions--elevenOptionClassStack--142", + "case": "exceptions", + "profile": "elevenOptionClassStack", + "seed": 142, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": true, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": true, + "specializedOpcodes": true, + "aliasedOpcodes": true, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": true, + "handlerTable": true, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "emitted-representation-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true, + "concealConstants": true, + "antiInstrumentation": true, + "specializedOpcodes": true, + "macroOpcodes": true, + "aliasedOpcodes": true, + "shuffleOpcodes": true, + "handlerTable": true, + "classObfuscation": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/class-handler-alias-stack-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/class-runtime-boot/encoded.js b/test/vm/jsconfuser-vm/fixtures/class-runtime-boot/encoded.js new file mode 100644 index 00000000..9a1c4b14 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/class-runtime-boot/encoded.js @@ -0,0 +1,959 @@ +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 13, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(constants, fake_14, globals, fake_13, bytecode) { + this.r = bytecode; + this.ad = fake_13; + this.J = constants; + this.I = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.c = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.U = []; + this.ag = 1; + this.G = 0; + this.W = fake_14; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) + +// Closure & Frame +function Closure(fn, fake_11, fake_10, fake_12) { + this.L = fake_12 && fake_11; + this.o = fn; + this.E = []; + this.prototype = {}; // <- default prototype object for `new` + this.q = fake_10; +} +function Upvalue(fake_3, fake_2, regs, fake_1, absSlot) { + this.U = regs; // shared reference to VM._regs flat array + this.u = fake_2; + this.N = absSlot; // absolute index; stable as long as frame is alive + this.H = fake_3; + this.l = fake_1; + this.Y = false; + this.v = undefined; +} +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +VM.prototype.m = function (fp, fake_26) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.c; + if (!uvs) return; // nothing in this program ever captured a local + this.S = fake_26; + var regs = this.U; + var hi = fp + regs[fp + 6]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.w(undefined); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.y = function (fake_18, fake_20, retDst, fake_19, args, thisVal, closure, fake_17) { + var fn = closure.o; + var regs = this.U; + this.s = fake_19; + var fp = this.ag; + var size = 8 + fn.M; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 8; + regs[fp + 0] = fn.X; + regs[fp + 1] = this.G; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.b; + if (typeof void 0 === "number") regs[fp + void 0] = args; + this.Z = fake_17; + if (typeof void 0 === "number") regs[fp + void 0] = fn.X; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.ag = end; + this.n = [fake_18, fake_20]; + if (args) { + if (fn.A) { + var restSlot = fn.b - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.M; a++) regs[base + a] = args[a]; + } + if (fn.b < fn.M) regs[base + fn.b] = args; + } + this.G = fp; +}; +Upvalue.prototype.w = function (fake_9) { + this.j = fake_9; + this.v = this.U[this.N]; + this.Y = true; +}; +Upvalue.prototype.ac = function (fake_4, fake_5) { + return this.Y ? this.v : this.U[this.N]; + this.D = fake_5; + this.af = fake_4; +}; +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.x = function (keyIn, fake_23, fake_24, fake_25, idxIn) { + var idx = idxIn ?? this.Q({}, null); + this.T = fake_23; + var key = keyIn ?? this.Q({}, 95); + var v = this.J[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + this.R = fake_25; + this.F = fake_24; + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.Q = function (fake_15, fake_16) { + this.ab = fake_16 && fake_15; + return this.r[this.U[this.G + 0]++]; +}; +VM.prototype.i = function (slot, fake_21, fp, fake_22) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.U[fp + 7] + slot; + this.C = fake_22; + var uvs = this.c || (this.c = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(undefined, [], this.U, "k", absSlot)); + this.V = fake_21; +}; +Upvalue.prototype.e = function (fake_8, fake_7, v, fake_6) { + this.B = fake_8 || fake_6; + this.t = fake_7; + if (this.Y) this.v = v;else this.U[this.N] = v; +}; +VM.prototype.k = function (closure, fake_29, thisVal, fake_28, fake_30, fake_27, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.a = [fake_28, fake_30]; + this.y({}, [], 0, {}, args, thisVal, closure, undefined); + while (true) { + var fp = this.G; + var regs = this.U; + var bc = this.r; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.r[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.r.length; i++) this.r[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.ag; i2++) this.U[i2] = undefined; + op = 39; + regs[fp + 0] = this.r.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 0: + { + var dst = this.Q([], "q"); + regs[base + dst] = this.x(); + break; + } + case 1: + { + var dst = this.Q(undefined, 11); + regs[base + dst] = this.Q("x", undefined); + break; + } + case 2: + { + var dst = this.Q("z", "q"); + var globalName = this.x(); + if (!(globalName in this.I)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.I[globalName]; + break; + } + case 3: + { + var dst = this.Q([], {}); + regs[base + dst] = regs[fp + 4].E[this.Q(undefined, {})].ac({}, "q"); + break; + } + case 4: + { + var dst = this.Q([], undefined); + regs[base + dst] = regs[fp + 3]; + break; + } + case 5: + { + var dst = this.Q(null, undefined); + regs[base + dst] = regs[base + this.Q(null, [])]; + break; + } + case 6: + { + // globals[globalName] = regs[src] + this.I[this.x()] = regs[base + this.Q(null, [])]; + break; + } + case 7: + { + var uvIdx = this.Q({}, undefined); + regs[fp + 4].E[uvIdx].e({}, [], regs[base + this.Q([], {})], undefined); + break; + } + case 8: + { + // dst = regs[obj][regs[key]] + var dst = this.Q({}, []); + var obj = regs[base + this.Q(null, {})]; + var key = regs[base + this.Q([], [])]; + regs[base + dst] = obj[key]; + break; + } + case 9: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.Q(undefined, null)]; + var key = regs[base + this.Q(null, [])]; + var val = regs[base + this.Q({}, 67)]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 10: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.Q([], {}); + var obj = regs[base + this.Q(null, "q")]; + var key = regs[base + this.Q(undefined, "z")]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 11: + { + var dst = this.Q(undefined, {}); + var a = regs[base + this.Q([], 91)]; + regs[base + dst] = a + regs[base + this.Q({}, 90)]; + break; + } + case 12: + { + var dst = this.Q({}, null); + var a = regs[base + this.Q(54, "x")]; + regs[base + dst] = a - regs[base + this.Q([], "q")]; + break; + } + case 13: + { + var dst = this.Q(null, "k"); + var a = regs[base + this.Q([], 91)]; + regs[base + dst] = a * regs[base + this.Q(null, {})]; + break; + } + case 14: + { + var dst = this.Q(null, 24); + var a = regs[base + this.Q("q", {})]; + regs[base + dst] = a / regs[base + this.Q([], "z")]; + break; + } + case 15: + { + var dst = this.Q(30, undefined); + var a = regs[base + this.Q([], [])]; + regs[base + dst] = a % regs[base + this.Q([], undefined)]; + break; + } + case 60: + { + // Math.pow instead of `**` + var dst = this.Q(null, null); + var a = regs[base + this.Q(null, 18)]; + regs[base + dst] = Math.pow(a, regs[base + this.Q({}, "z")]); + break; + } + case 16: + { + var dst = this.Q([], 96); + var a = regs[base + this.Q({}, "z")]; + regs[base + dst] = a & regs[base + this.Q({}, 4)]; + break; + } + case 17: + { + var dst = this.Q(undefined, 19); + var a = regs[base + this.Q(24, "x")]; + regs[base + dst] = a | regs[base + this.Q({}, "y")]; + break; + } + case 18: + { + var dst = this.Q(undefined, "y"); + var a = regs[base + this.Q([], {})]; + regs[base + dst] = a ^ regs[base + this.Q(null, [])]; + break; + } + case 19: + { + var dst = this.Q("z", []); + var a = regs[base + this.Q(undefined, [])]; + regs[base + dst] = a << regs[base + this.Q(24, "z")]; + break; + } + case 20: + { + var dst = this.Q("q", {}); + var a = regs[base + this.Q(undefined, 17)]; + regs[base + dst] = a >> regs[base + this.Q({}, undefined)]; + break; + } + case 21: + { + var dst = this.Q([], "y"); + var a = regs[base + this.Q("q", 0)]; + regs[base + dst] = a >>> regs[base + this.Q({}, {})]; + break; + } + + // Comparison (dst, src1, src2) + case 22: + { + var dst = this.Q([], 58); + var a = regs[base + this.Q(87, null)]; + regs[base + dst] = a < regs[base + this.Q("y", 60)]; + break; + } + case 23: + { + var dst = this.Q(60, "z"); + var a = regs[base + this.Q({}, 6)]; + regs[base + dst] = a > regs[base + this.Q(null, "q")]; + break; + } + case 24: + { + var dst = this.Q("z", "x"); + var a = regs[base + this.Q("x", [])]; + regs[base + dst] = a <= regs[base + this.Q({}, {})]; + break; + } + case 25: + { + var dst = this.Q(undefined, "k"); + var a = regs[base + this.Q({}, null)]; + regs[base + dst] = a >= regs[base + this.Q(undefined, 79)]; + break; + } + case 26: + { + var dst = this.Q([], 1); + var a = regs[base + this.Q("y", [])]; + regs[base + dst] = a === regs[base + this.Q("k", null)]; + break; + } + case 27: + { + var dst = this.Q({}, {}); + var a = regs[base + this.Q({}, 83)]; + regs[base + dst] = a !== regs[base + this.Q([], null)]; + break; + } + case 28: + { + var dst = this.Q(undefined, 75); + var a = regs[base + this.Q({}, undefined)]; + regs[base + dst] = a == regs[base + this.Q("y", "x")]; + break; + } + case 29: + { + var dst = this.Q(undefined, {}); + var a = regs[base + this.Q([], [])]; + regs[base + dst] = a != regs[base + this.Q({}, "z")]; + break; + } + case 30: + { + var dst = this.Q(null, "z"); + var a = regs[base + this.Q(null, [])]; + regs[base + dst] = a in regs[base + this.Q({}, "q")]; + break; + } + case 31: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.Q(null, undefined); + var obj = regs[base + this.Q({}, {})]; + regs[base + dst] = obj instanceof regs[base + this.Q(60, null)]; + break; + } + + // Unary (dst, src) + case 32: + { + var dst = this.Q("y", "y"); + regs[base + dst] = -regs[base + this.Q(undefined, null)]; + break; + } + case 33: + { + var dst = this.Q({}, "y"); + regs[base + dst] = +regs[base + this.Q([], 82)]; + break; + } + case 34: + { + var dst = this.Q(null, "k"); + regs[base + dst] = !regs[base + this.Q("z", [])]; + break; + } + case 35: + { + var dst = this.Q({}, null); + regs[base + dst] = ~regs[base + this.Q({}, 74)]; + break; + } + case 36: + { + var dst = this.Q("y", {}); + regs[base + dst] = typeof regs[base + this.Q({}, [])]; + break; + } + case 37: + { + var dst = this.Q("q", []); + this.Q({}, undefined); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 38: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.Q({}, 50); + var name = this.x(); + var val = Object.prototype.hasOwnProperty.call(this.I, name) ? this.I[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39: + regs[fp + 0] = this.Q({}, 33); + break; + case 40: + { + var src = this.Q(67, undefined); + var target = this.Q("q", undefined); + if (!regs[base + src]) regs[fp + 0] = target; + break; + } + case 41: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.Q("q", null); + var target = this.Q(null, {}); + if (regs[base + src]) regs[fp + 0] = target; + break; + } + + // Calls + case 42: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.Q({}, null); + var callee = regs[base + this.Q(null, [])]; + var argc = this.Q(null, {}); + var args; + if (argc === 65535) { + args = regs[base + this.Q({}, 66)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.Q(undefined, 53)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.y(undefined, {}, dst << 1, [], args, this.I, closure, null); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 43: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.Q("z", 70); + var receiver = regs[base + this.Q(10, null)]; + var callee = regs[base + this.Q({}, undefined)]; + var argc = this.Q(undefined, {}); + var args; + if (argc === 65535) { + args = regs[base + this.Q(1, [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.Q("z", [])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.y({}, 20, dst << 1, 55, args, receiver, closure, []); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 44: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.Q(undefined, []); + var callee = regs[base + this.Q(null, "x")]; + var argc = this.Q(76, {}); + var args; + if (argc === 65535) { + args = regs[base + this.Q({}, [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.Q(73, 28)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.y("y", 0, dst << 1 | 1, {}, args, Object.create(closure.prototype || null), closure, 95); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 45: + { + var retVal = regs[base + this.Q([], undefined)]; + this.m(fp, []); // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.ag = fp; + this.G = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 46: + throw regs[base + this.Q(null, [])]; + + // Closures + case 47: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.Q(undefined, {}); + var startPc = this.Q({}, "z"); + var paramCount = this.Q([], null); + var regCount = this.Q(undefined, undefined); + var uvCount = this.Q({}, 48); + var hasRest = this.Q(undefined, "q"); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.Q(null, {}); + var uvIndex = this.Q(91, "y"); + uvDescs[i] = { + K: isLocalRaw, + z: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + b: paramCount, + M: regCount, + X: startPc, + A: hasRest + }; + var closure = new Closure(fn, null, null, {}); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.K) { + closure.E.push(this.i(uvd.z, 74, fp, undefined)); + } else { + closure.E.push(regs[fp + 4].E[uvd.z]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.J, {}, self.I, 32, self.r).k(c, {}, this == null ? self.I : this, [], 52, {}, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 48: + { + // dst, count, [elemReg...] + var dst = this.Q(null, []); + var count = this.Q("z", undefined); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.Q("z", null)]; + regs[base + dst] = elems; + break; + } + case 49: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.Q([], 75); + var pairCount = this.Q({}, null); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.Q(null, null)]; + var val = regs[base + this.Q("x", null)]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50: + { + // obj, key, fn + var obj = regs[base + this.Q("z", undefined)]; + var key = regs[base + this.Q({}, 41)]; + var getterFn = regs[base + this.Q({}, [])]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 51: + { + // obj, key, fn + var obj = regs[base + this.Q(null, null)]; + var key = regs[base + this.Q([], undefined)]; + var setterFn = regs[base + this.Q({}, {})]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.Q(32, []); + var obj = regs[base + this.Q(null, null)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + aa: keys, + p: 0 + }; + break; + } + case 53: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.Q("z", "q"); + var iter = regs[base + this.Q({}, {})]; + var exitTarget = this.Q("x", "z"); + if (iter.p >= iter.aa.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.aa[iter.p++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 54: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + O: this.Q(undefined, {}), + f: this.Q(null, []) + }); + break; + } + case 55: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 59: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + h: this.Q([], {}), + g: this.Q("k", undefined), + d: this.Q("z", {}), + ae: this.Q(61, null) + }); + break; + } + + // Self-modifying bytecode + case 56: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.Q("q", []); + var sliceStart = this.Q(null, 4); + var sliceEnd = this.Q(75, "q"); + var pk = this.Q(undefined, null) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.r[destPc + (pi - sliceStart)] = (this.r[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 58: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.Q([], null)]; + break; + } + case 57: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 0] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.G; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.m(searchFrame, undefined); + this.ag = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.G = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.f !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.f] = err; + regs[handledFrame + 0] = h.O; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.g] = h.ae; + regs[hBase + h.d] = err; + regs[handledFrame + 0] = h.h; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.ag = handledFrame + regs[handledFrame + 6]; + this.G = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.G) return this.U[0]; + } + this.P = fake_27 || fake_29; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(CONSTANTS, undefined, globals, "z", decodeBytecode(BYTECODE)); +vm.k(new Closure({ + b: 0, + M: 7, + X: 0 +}, null, undefined, null), 66, undefined, "z", undefined, null, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/class-runtime-boot/qualification.json b/test/vm/jsconfuser-vm/fixtures/class-runtime-boot/qualification.json new file mode 100644 index 00000000..9eca17c1 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/class-runtime-boot/qualification.json @@ -0,0 +1,58 @@ +{ + "id": "arithmetic--classObfuscation--1", + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "case": "arithmetic", + "profile": "classObfuscation", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": true, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "container-declined", + "message": "extractContainerFromSource declined the input container" + }, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": null, + "effectObservation": { + "axis": "runtime-member-layout", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/class-runtime-boot/source.js b/test/vm/jsconfuser-vm/fixtures/class-runtime-boot/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/class-runtime-boot/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/encoded.js b/test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/encoded.js new file mode 100644 index 00000000..a9f2b616 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/encoded.js @@ -0,0 +1,1108 @@ +var CONSTANTS = [/* 0 */"6/fwktQOzIGVGQ==", /* 1 */1599533299, /* 2 */"+Cj7of/ZyFDV+NBH", /* 3 */"CGyG9BUEqY0XPJmUBuSEf2XUb1R8PA==", /* 4 */67297407, /* 5 */-1368020393, /* 6 */"lY0eBryCNA67syMm2uKkfQ==", /* 7 */undefined, /* 8 */"GRWqevoP4pvWNj+tlx0=", /* 9 */"TKm5IS2pI1Mg/w==", /* 10 */2039295864, /* 11 */-1420456353, /* 12 */"cqtKP1+LSQWR/Q==", /* 13 */"qTk5rqnaIGaw6yaerAoxhg==", /* 14 */926573267, /* 15 */"cRx3tEQMT5xo9GF0YRw=", /* 16 */true]; +var BYTECODE = [0, 3, 0, 2997876460, 0, 4, 1, 1599515422, 4, 1, 47, 5, 92, 1, 12, 0, 0, 61, 3, 5, 3, 1, 4, 2, 3, 3, 2, 5, 2, 3040885113, 0, 6, 3, 4227744824, 0, 7, 4, 67297406, 62, 4, 3, 3, 3, 3, 4, 8, 4, 7, 4, 4, 42, 7, 2, 1, 8, 0, 8, 5, 2926946899, 42, 9, 2, 1, 8, 2, 8, 2, 3040885113, 0, 10, 6, 2474870386, 8, 11, 8, 10, 48, 8, 3, 7, 9, 11, 9, 5, 6, 8, 0, 5, 7, 0, 45, 5, 0, 8, 8, 3297348009, 0, 7, 9, 3445586966, 0, 6, 10, 2039320910, 4, 2, 59, 208, 4, 5, 226, 54, 179, 3, 0, 9, 11, 2874510943, 63, 7, 0, 0, 9, 6, 6, 10, 1, 6, 40, 10, 146, 2, 9, 12, 2958714368, 0, 10, 13, 160337470, 44, 11, 9, 1, 10, 46, 11, 0, 9, 4, 67297406, 64, 8, 7, 14, 7, 7, 9, 10, 926584981, 0, 55, 61, 7, 10, 7, 1, 7, 5, 8, 7, 1, 4, 228, 55, 39, 208, 55, 39, 202, 0, 9, 15, 2026711680, 8, 10, 3, 9, 61, 6, 10, 8, 0, 6, 5, 8, 6, 1, 4, 230, 55, 39, 208, 55, 1, 4, 232, 39, 208, 2, 9, 2, 3040885113, 0, 10, 6, 2474870386, 0, 11, 16, 0, 9, 9, 10, 11, 58, 4, 46, 5, 45, 5, 45, 5, 0, 9, 7, 0, 45, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_5_1_7_3_4_0_6_2 (order: [5,1,7,3,4,0,6,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[2]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[2]; + var pairCount = _operands[3]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[4]]; + var val = regs[base + _operands[5]]; + o[key] = val; + } + regs[base + dst] = o; + // TYPEOF + var dst = _operands[6]; + regs[base + dst] = typeof regs[base + _operands[7]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_10_9_4_5_8_7_0_2_1_6_3 (order: [10,9,4,5,8,7,0,2,1,6,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[0]]; + // UNARY_NEG + var dst = _operands[0]; + regs[base + dst] = -regs[base + _operands[1]]; + // MUL + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a * regs[base + _operands[4]]; + // LTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a <= regs[base + _operands[7]]; + // BXOR + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a ^ regs[base + _operands[10]]; + break; + } + case 63: + { + // ANTI_LT_3_4_1_2_8_6_0_7_5 (order: [3,4,1,2,8,6,0,7,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[4]]; + // LT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[3]; + var count = _operands[4]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[5]]; + regs[base + dst] = elems; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[6]; + var count = _operands[7]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[8]]; + regs[base + dst] = elems; + break; + } + case 64: + { + // ANTI_ADD_8_0_1_7_6_5_3_2_4 (order: [8,0,1,7,6,5,3,2,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[0]]; + // LOAD_CONST + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/qualification.json b/test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/qualification.json new file mode 100644 index 00000000..61be39c0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/qualification.json @@ -0,0 +1,73 @@ +{ + "id": "exceptions--concealConstantsAntiInstrumentation--74", + "case": "exceptions", + "profile": "concealConstantsAntiInstrumentation", + "seed": 74, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "concealConstants": true, + "antiInstrumentation": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/source.js b/test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-anti-instrumentation/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/dead-code/encoded.js b/test/vm/jsconfuser-vm/fixtures/dead-code/encoded.js new file mode 100644 index 00000000..eb404276 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dead-code/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */undefined, /* 4 */"g", /* 5 */2, /* 6 */3]; +var BYTECODE = [4, 1, 47, 4, 63, 1, 6, 0, 0, 5, 2, 4, 47, 4, 107, 1, 7, 0, 0, 5, 3, 4, 2, 4, 0, 0, 0, 5, 1, 0, 0, 6, 2, 0, 42, 7, 2, 1, 6, 0, 6, 2, 0, 42, 8, 3, 1, 6, 48, 6, 2, 7, 8, 9, 4, 5, 6, 0, 4, 3, 0, 45, 4, 4, 2, 0, 4, 2, 0, 11, 5, 0, 4, 45, 5, 54, 92, 3, 2, 4, 4, 0, 42, 5, 4, 0, 5, 0, 5, 55, 39, 99, 0, 4, 5, 0, 5, 0, 4, 45, 0, 0, 4, 3, 0, 45, 4, 4, 2, 0, 5, 2, 0, 11, 6, 0, 5, 45, 6, 59, 141, 3, 4, 150, 2, 5, 4, 0, 42, 6, 5, 0, 5, 0, 6, 55, 1, 3, 152, 39, 141, 0, 5, 6, 0, 5, 0, 5, 58, 3, 46, 4, 45, 0, 0, 5, 3, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/dead-code/generation.json b/test/vm/jsconfuser-vm/fixtures/dead-code/generation.json new file mode 100644 index 00000000..8ca5853d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dead-code/generation.json @@ -0,0 +1,11 @@ +{ + "note": "encoded.js is the VM encoder's output for source.js with no options, the leading disassembly comment block removed (the committed corpus format). Math.random was replaced by the seeded PRNG below (mulberry32: state += 0x6d2b79f5, then the standard mulberry32 output). Regenerating with the same encoder revision, options and seed reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5" + }, + "files": { + "encoded.js": { "options": {}, "seed": 1 } + }, + "claim": "dead code after a return: the compiler emits a try/catch and a try/finally no path reaches, with calls inside and the finalizer's resume JUMP_REG; the backend's control solver reaches none of them, so the program decodes with the source's behaviour" +} diff --git a/test/vm/jsconfuser-vm/fixtures/dead-code/source.js b/test/vm/jsconfuser-vm/fixtures/dead-code/source.js new file mode 100644 index 00000000..151797cb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dead-code/source.js @@ -0,0 +1,13 @@ +// Dead code after a return: a try/catch and a try/finally the compiler still emits, with calls in +// both, and a finalizer resume nothing reaches. +function deadCatch(x) { + return x + 1 + try { x = g() } catch (e) { x = 2 } + return x +} +function deadFinally(x) { + return x + 1 + try { x = g() } finally { x = 3 } + return x +} +window.TEST_OUTPUT = [deadCatch(1), deadFinally(1)] diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/encoded.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/encoded.js new file mode 100644 index 00000000..54ac1ff6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/encoded.js @@ -0,0 +1,1355 @@ +var CONSTANTS = [/* 0 */12262, /* 1 */16482, /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */1, /* 5 */3, /* 6 */undefined, /* 7 */6175, /* 8 */45511, /* 9 */0, /* 10 */26048, /* 11 */7029, /* 12 */"NaN", /* 13 */"XiAcxUkis", /* 14 */2, /* 15 */"parseFloat", /* 16 */"Rim1af8g", /* 17 */"HMehH0Q", /* 18 */"one", /* 19 */"two", /* 20 */"other", /* 21 */"p1bi", /* 22 */"Q7lB", /* 23 */10452, /* 24 */"VWvz", /* 25 */16, /* 26 */"Number", /* 27 */838477237, /* 28 */"Math", /* 29 */"imul", /* 30 */3598250135, /* 31 */"isNaN", /* 32 */21]; +var BYTECODE = [0, 4, 0, 0, 0, 3, 1, 0, 4, 1, 47, 5, 72, 1, 16, 0, 0, 61, 3, 3, 4, 3, 3, 3, 2, 3, 3, 3, 5, 4, 4, 2, 5, 2, 0, 0, 6, 3, 0, 0, 7, 4, 0, 42, 8, 2, 1, 7, 0, 7, 5, 0, 42, 9, 2, 1, 7, 48, 7, 2, 8, 9, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 0, 12, 7, 0, 0, 10, 8, 0, 0, 13, 9, 0, 0, 11, 10, 0, 47, 6, 365, 3, 13, 0, 0, 4, 2, 0, 14, 4, 0, 62, 11, 0, 15, 14, 10, 11, 10, 11, 10, 11, 11, 2, 10, 10, 10, 63, 2, 11, 8, 15, 11, 10, 11, 11, 64, 11, 10, 8, 10, 10, 10, 11, 8, 65, 11, 10, 0, 10, 8, 10, 10, 11, 11, 8, 10, 1, 3, 4101977722, 1, 9, 859853015, 66, 10, 0, 9, 11, 3, 12, 10, 11, 0, 9, 67, 8, 9, 12, 172, 11, 10, 9, 68, 11, 0, 10, 13, 11, 10, 11, 9, 10, 1, 10, 11, 3, 3, 10, 1, 4, 15053, 1, 5, 4990, 39, 348, 1, 3, 2275197283, 1, 4, 22612, 1, 5, 56740, 39, 348, 0, 14, 14, 0, 62, 11, 0, 15, 14, 11, 11, 10, 10, 10, 10, 11, 2, 11, 10, 11, 63, 2, 10, 8, 15, 10, 10, 10, 10, 64, 10, 10, 8, 10, 10, 10, 10, 8, 1, 3, 397032586, 1, 9, 3512977015, 66, 10, 2, 9, 10, 3, 15, 10, 10, 0, 9, 69, 8, 10, 10, 10, 10, 10, 10, 10, 9, 0, 9, 16, 10, 10, 10, 68, 10, 0, 10, 17, 11, 11, 11, 9, 11, 0, 11, 10, 3, 3, 11, 1, 4, 30231, 1, 5, 53344, 39, 348, 1, 3, 29380112, 1, 4, 31945, 1, 5, 7814, 39, 348, 1, 3, 2309685993, 1, 4, 21770, 1, 5, 18916, 39, 348, 0, 14, 18, 0, 45, 14, 0, 14, 19, 0, 45, 14, 0, 14, 20, 0, 45, 14, 0, 14, 6, 0, 45, 14, 42, 3, 6, 3, 3, 4, 5, 0, 7, 21, 0, 8, 3, 3, 7, 58, 3, 0, 7, 22, 0, 0, 6, 23, 0, 0, 8, 24, 0, 4, 4, 0, 9, 25, 0, 70, 8, 8, 6, 10, 1, 2, 8, 8, 8, 7, 9, 7, 71, 8, 9, 8, 6, 45024, 10, 7, 1, 0, 10, 9, 0, 72, 6, 6, 6, 10, 6, 9, 11, 6, 6, 6, 8, 6, 8, 61, 8, 7, 7, 8, 6, 7, 5, 6, 8, 8, 11, 7, 7, 73, 7, 8, 0, 6, 5, 9, 31313, 0, 10, 9, 0, 72, 6, 8, 6, 10, 6, 9, 11, 8, 8, 6, 7, 6, 7, 61, 8, 6, 7, 7, 8, 7, 0, 7, 7, 6, 11, 6, 8, 74, 8, 7, 7, 0, 6, 0, 9, 6, 6, 26, 7, 6, 7, 0, 10, 9, 0, 72, 7, 6, 8, 10, 8, 9, 11, 7, 8, 6, 8, 6, 7, 61, 6, 7, 7, 7, 6, 7, 0, 8, 8, 7, 11, 8, 8, 0, 9, 27, 0, 73, 6, 6, 0, 7, 9, 10, 56494, 0, 9, 9, 0, 72, 6, 8, 7, 9, 7, 10, 11, 6, 6, 7, 7, 6, 8, 61, 6, 7, 6, 8, 6, 6, 0, 8, 6, 6, 11, 7, 7, 2, 9, 28, 0, 0, 10, 29, 0, 8, 11, 9, 10, 0, 10, 30, 0, 43, 12, 9, 11, 2, 0, 10, 0, 9, 9, 0, 72, 8, 7, 7, 9, 8, 12, 10, 6, 8, 6, 8, 7, 6, 61, 8, 6, 6, 8, 6, 8, 0, 7, 6, 7, 10, 7, 6, 74, 8, 8, 8, 0, 7, 0, 9, 8, 7, 31, 8, 6, 8, 0, 10, 9, 0, 72, 6, 7, 7, 10, 6, 9, 11, 8, 8, 8, 6, 7, 6, 61, 6, 7, 7, 7, 7, 6, 0, 7, 7, 7, 11, 7, 7, 0, 9, 32, 0, 72, 6, 6, 7, 9, 6, 0, 10, 7, 7, 7, 6, 7, 7, 73, 7, 6, 0, 7, 10, 9, 34121, 0, 10, 9, 0, 72, 6, 6, 6, 10, 6, 9, 11, 6, 6, 7, 7, 6, 6, 61, 6, 6, 7, 6, 6, 7, 0, 7, 7, 6, 11, 7, 7, 0, 9, 21, 0, 49, 10, 1, 9, 0, 45, 10, 0, 9, 6, 0, 45, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_3_11_12_4_2_8_0_6_5_9_1_7_10 (order: [3,11,12,4,2,8,0,6,5,9,1,7,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[12], _unsortedOperands[1], _unsortedOperands[2]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // UNARY_NEG + var dst = _operands[2]; + regs[base + dst] = -regs[base + _operands[3]]; + // SHR + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a >> regs[base + _operands[6]]; + // MUL + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a * regs[base + _operands[9]]; + // SHR + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a >> regs[base + _operands[12]]; + break; + } + case 62: + { + // ANTI_EQ_8_1_0_2_14_5_9_7_11_12_3_4_13_6_10 (order: [8,1,0,2,14,5,9,7,11,12,3,4,13,6,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[10], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[13], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[14], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[12], _unsortedOperands[4]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[3]; + var count = _operands[4]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[5]]; + regs[base + dst] = elems; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + // SUB + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a - regs[base + _operands[11]]; + // LTE + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a <= regs[base + _operands[14]]; + break; + } + case 63: + { + // ANTI_UNARY_NOT_4_5_6_7_2_0_3_1 (order: [4,5,6,7,2,0,3,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[3]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[3]; + var count = _operands[4]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[5]]; + regs[base + dst] = elems; + // UNARY_NOT + var dst = _operands[6]; + regs[base + dst] = !regs[base + _operands[7]]; + break; + } + case 64: + { + // ANTI_UNARY_POS_1_0_7_5_2_4_3_6 (order: [1,0,7,5,2,4,3,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[2]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // UNARY_POS + var dst = _operands[6]; + regs[base + dst] = +regs[base + _operands[7]]; + break; + } + case 65: + { + // ANTI_UNARY_NEG_3_0_2_7_10_8_4_5_1_9_6 (order: [3,0,2,7,10,8,4,5,1,9,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[4]]; + // LOAD_CONST + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + // EXP + // Math.pow instead of `**` + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[5]]); + // MOD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a % regs[base + _operands[8]]; + // UNARY_NEG + var dst = _operands[9]; + regs[base + dst] = -regs[base + _operands[10]]; + break; + } + case 66: + { + // ANTI_SUB_5_4_7_3_9_1_6_0_2_8 (order: [5,4,7,3,9,1,6,0,2,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[4]]; + // LOAD_GLOBAL + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[3]; + var pairCount = _operands[4]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[5]]; + var val = regs[base + _operands[6]]; + o[key] = val; + } + regs[base + dst] = o; + // SUB + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a - regs[base + _operands[9]]; + break; + } + case 67: + { + // ANTI_BAND_3_2_5_6_1_0_4 (order: [3,2,5,6,1,0,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[3]]; + // UNARY_POS + var dst = _operands[0]; + regs[base + dst] = +regs[base + _operands[1]]; + // BAND + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a & regs[base + _operands[4]]; + // JUMP_IF_FALSE + var src = _operands[5]; + var target = _operands[6]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 68: + { + // ANTI_ADD_12_5_7_4_9_6_11_2_10_13_3_14_0_1_8 (order: [12,5,7,4,9,6,11,2,10,13,3,14,0,1,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[12], _unsortedOperands[13], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[14], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[11]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[3]; + var name = this._constant(_operands[4], _operands[5]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // ADD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a + regs[base + _operands[8]]; + // NEQ + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a !== regs[base + _operands[11]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[12]; + var count = _operands[13]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[14]]; + regs[base + dst] = elems; + break; + } + case 69: + { + // ANTI_MUL_7_3_2_11_10_0_12_5_8_14_6_13_9_4_1 (order: [7,3,2,11,10,0,12,5,8,14,6,13,9,4,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[14], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[13], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[12], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[11], _unsortedOperands[9]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // SHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >> regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + // SHR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a >> regs[base + _operands[11]]; + // LOAD_CONST + var dst = _operands[12]; + regs[base + dst] = this._constant(_operands[13], _operands[14]); + break; + } + case 70: + { + // ANTI_SHL_10_0_5_6_4_7_11_3_2_1_8_9 (order: [10,0,5,6,4,7,11,3,2,1,8,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[11], _unsortedOperands[0], _unsortedOperands[6]]; + // LOOSE_EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a == regs[base + _operands[2]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[3]; + var count = _operands[4]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[5]]; + regs[base + dst] = elems; + // SHL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a << regs[base + _operands[8]]; + // SHR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a >> regs[base + _operands[11]]; + break; + } + case 71: + { + // ANTI_BOR_4_0_6_3_7_2_5_1 (order: [4,0,6,3,7,2,5,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[4]]; + // BOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a | regs[base + _operands[2]]; + // MUL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a * regs[base + _operands[5]]; + // LOAD_INT + var dst = _operands[6]; + regs[base + dst] = _operands[7]; + break; + } + case 72: + { + // ANTI_USHR_11_6_12_2_3_1_0_5_7_4_10_9_8 (order: [11,6,12,2,3,1,0,5,7,4,10,9,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[12], _unsortedOperands[11], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[2]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // UNARY_POS + var dst = _operands[3]; + regs[base + dst] = +regs[base + _operands[4]]; + // UNARY_POS + var dst = _operands[5]; + regs[base + dst] = +regs[base + _operands[6]]; + // NEQ + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a !== regs[base + _operands[9]]; + // BXOR + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a ^ regs[base + _operands[12]]; + break; + } + case 73: + { + // ANTI_BXOR_1_0_3_5_4_2_6 (order: [1,0,3,5,4,2,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[6]]; + // UNARY_NEG + var dst = _operands[0]; + regs[base + dst] = -regs[base + _operands[1]]; + // BXOR + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a ^ regs[base + _operands[4]]; + // LOAD_INT + var dst = _operands[5]; + regs[base + dst] = _operands[6]; + break; + } + case 74: + { + // ANTI_UNARY_BITNOT_7_10_2_1_9_12_0_6_5_11_4_8_3 (order: [7,10,2,1,9,12,0,6,5,11,4,8,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[12], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[5]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // SUB + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a - regs[base + _operands[4]]; + // EQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a === regs[base + _operands[7]]; + // UNARY_NEG + var dst = _operands[8]; + regs[base + dst] = -regs[base + _operands[9]]; + // LOAD_GLOBAL + var dst = _operands[10]; + var globalName = this._constant(_operands[11], _operands[12]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/qualification.json b/test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/qualification.json new file mode 100644 index 00000000..36a85c38 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/qualification.json @@ -0,0 +1,70 @@ +{ + "id": "switch--dispatcherAntiInstrumentation--81", + "case": "switch", + "profile": "dispatcherAntiInstrumentation", + "seed": 81, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": true, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "dispatcher": true, + "antiInstrumentation": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/source.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/source.js new file mode 100644 index 00000000..98a67db9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-anti-instrumentation-switch/source.js @@ -0,0 +1 @@ +function classify(value) { switch (value) { case 1: return "one"; case 2: return "two"; default: return "other"; } } window.TEST_OUTPUT = [classify(1), classify(3)]; diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/encoded.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/encoded.js new file mode 100644 index 00000000..3a43ec03 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */4, /* 2 */2, /* 3 */1, /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */undefined, /* 7 */1986124437, /* 8 */27, /* 9 */5, /* 10 */"Math", /* 11 */"imul", /* 12 */1664908847, /* 13 */463855522, /* 14 */2409275238, /* 15 */28]; +var BYTECODE = [47, 6, 177, 2, 8, 0, 0, 4, 1, 0, 10, 0, 0, 5, 2, 10, 0, 10, 0, 0, 5, 3, 10, 0, 10, 1, 0, 22, 11, 3, 10, 34, 8, 11, 33, 8, 8, 32, 8, 8, 1, 4, 2195090663, 1, 9, 1138795783, 12, 9, 9, 4, 16, 9, 8, 9, 11, 4, 4, 9, 1, 5, 1051880745, 39, 162, 0, 10, 2, 0, 15, 11, 3, 10, 34, 8, 11, 33, 8, 8, 1, 4, 2991723466, 1, 9, 3952685859, 12, 9, 9, 4, 13, 9, 9, 8, 11, 4, 4, 9, 1, 5, 4231085707, 39, 162, 11, 10, 2, 3, 5, 2, 10, 1, 4, 3402410250, 1, 5, 532268963, 39, 162, 12, 10, 2, 3, 5, 2, 10, 5, 10, 3, 0, 10, 3, 0, 11, 11, 3, 10, 5, 3, 11, 1, 4, 2095005023, 1, 5, 1905279965, 39, 162, 2, 10, 4, 0, 0, 11, 5, 0, 9, 10, 11, 2, 0, 10, 6, 0, 45, 10, 42, 4, 6, 2, 5, 4, 1, 7, 0, 8, 4, 4, 7, 58, 4, 4, 3, 0, 4, 7, 0, 12, 5, 1, 4, 0, 4, 0, 0, 21, 6, 5, 4, 5, 1, 6, 0, 4, 8, 0, 19, 5, 1, 4, 0, 4, 9, 0, 21, 6, 1, 4, 17, 4, 5, 6, 0, 5, 0, 0, 21, 6, 4, 5, 5, 1, 6, 2, 4, 10, 0, 0, 5, 11, 0, 8, 6, 4, 5, 0, 5, 12, 0, 43, 7, 4, 6, 2, 1, 5, 0, 4, 0, 0, 21, 5, 7, 4, 5, 1, 5, 0, 4, 13, 0, 11, 5, 1, 4, 0, 4, 0, 0, 21, 6, 5, 4, 5, 1, 6, 18, 4, 1, 0, 0, 5, 0, 0, 21, 6, 4, 5, 5, 1, 6, 0, 4, 14, 0, 18, 5, 1, 4, 0, 4, 0, 0, 21, 6, 5, 4, 5, 1, 6, 0, 4, 15, 0, 21, 5, 1, 4, 18, 4, 1, 5, 0, 5, 0, 0, 21, 6, 4, 5, 5, 1, 6, 48, 4, 1, 1, 45, 4, 0, 4, 6, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/qualification.json b/test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/qualification.json new file mode 100644 index 00000000..0042969a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "branches--dispatcher--32", + "case": "branches", + "profile": "dispatcher", + "seed": 32, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": true, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 2 + }, + "encodedObservation": { + "kind": "value", + "value": 2 + }, + "decodedObservation": { + "kind": "value", + "value": 2 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/source.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/source.js new file mode 100644 index 00000000..a2f57916 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-array-result/source.js @@ -0,0 +1 @@ +let value = 0; for (let i = 0; i < 4; i++) { if (i % 2) value += i; else value -= i; } window.TEST_OUTPUT = value; diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-branches/encoded.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-branches/encoded.js new file mode 100644 index 00000000..cf15ebc8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-branches/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */4, /* 2 */2, /* 3 */1, /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */undefined, /* 7 */"w0ze", /* 8 */27, /* 9 */"Math", /* 10 */"imul", /* 11 */1840398735, /* 12 */1219511517, /* 13 */352380081]; +var BYTECODE = [47, 6, 181, 2, 8, 0, 0, 4, 1, 0, 10, 0, 0, 5, 2, 10, 0, 10, 0, 0, 5, 3, 10, 0, 10, 1, 0, 22, 11, 3, 10, 34, 8, 11, 33, 8, 8, 32, 8, 8, 1, 4, 2350980728, 1, 9, 915338747, 12, 9, 9, 4, 16, 9, 8, 9, 11, 4, 4, 9, 1, 5, 1435190715, 39, 165, 0, 10, 2, 0, 15, 11, 3, 10, 34, 8, 11, 33, 8, 8, 32, 8, 8, 1, 4, 436147527, 1, 9, 3861183455, 12, 9, 9, 4, 16, 9, 9, 8, 11, 4, 4, 9, 1, 5, 3548103992, 39, 165, 11, 10, 2, 3, 5, 2, 10, 1, 4, 2751428588, 1, 5, 1206557658, 39, 165, 12, 10, 2, 3, 5, 2, 10, 5, 10, 3, 0, 10, 3, 0, 11, 11, 3, 10, 5, 3, 11, 1, 4, 3370973527, 1, 5, 3258495102, 39, 165, 2, 10, 4, 0, 0, 11, 5, 0, 9, 10, 11, 2, 0, 10, 6, 0, 45, 10, 42, 4, 6, 2, 4, 5, 0, 7, 7, 0, 8, 4, 4, 7, 58, 4, 4, 3, 0, 4, 8, 0, 21, 5, 0, 4, 18, 4, 0, 5, 0, 5, 0, 0, 21, 6, 4, 5, 5, 0, 6, 2, 4, 9, 0, 0, 5, 10, 0, 8, 6, 4, 5, 0, 5, 11, 0, 43, 7, 4, 6, 2, 0, 5, 0, 4, 0, 0, 21, 5, 7, 4, 5, 0, 5, 35, 4, 0, 0, 5, 0, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 12, 0, 11, 5, 0, 4, 0, 4, 0, 0, 21, 6, 5, 4, 5, 0, 6, 0, 4, 13, 0, 12, 5, 0, 4, 0, 4, 0, 0, 21, 6, 5, 4, 5, 0, 6, 11, 4, 0, 1, 0, 5, 0, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 7, 0, 49, 5, 1, 4, 0, 45, 5, 0, 4, 6, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-branches/qualification.json b/test/vm/jsconfuser-vm/fixtures/dispatcher-branches/qualification.json new file mode 100644 index 00000000..c89dfb0a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-branches/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "branches--dispatcher--28", + "case": "branches", + "profile": "dispatcher", + "seed": 28, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": true, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 2 + }, + "encodedObservation": { + "kind": "value", + "value": 2 + }, + "decodedObservation": { + "kind": "value", + "value": 2 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-branches/source.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-branches/source.js new file mode 100644 index 00000000..a2f57916 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-branches/source.js @@ -0,0 +1 @@ +let value = 0; for (let i = 0; i < 4; i++) { if (i % 2) value += i; else value -= i; } window.TEST_OUTPUT = value; diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/encoded.js new file mode 100644 index 00000000..438e0020 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */"finished", /* 5 */undefined, /* 6 */0, /* 7 */"Error", /* 8 */"negative", /* 9 */"message", /* 10 */true, /* 11 */"w0ze", /* 12 */27, /* 13 */"Math", /* 14 */"imul", /* 15 */1840398735, /* 16 */1219511517, /* 17 */352380081]; +var BYTECODE = [4, 1, 47, 3, 69, 1, 15, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 32, 6, 5, 42, 5, 2, 1, 6, 0, 6, 3, 0, 42, 7, 2, 1, 6, 2, 6, 0, 0, 0, 8, 4, 0, 8, 9, 6, 8, 48, 6, 3, 5, 7, 9, 9, 3, 4, 6, 0, 3, 5, 0, 45, 3, 47, 8, 255, 2, 8, 0, 0, 4, 2, 59, 209, 4, 5, 227, 54, 174, 3, 0, 12, 6, 0, 22, 13, 0, 12, 34, 10, 13, 33, 10, 10, 32, 10, 10, 1, 6, 1274249233, 1, 11, 404317844, 12, 11, 11, 6, 16, 11, 10, 11, 11, 6, 6, 11, 1, 7, 1435190715, 39, 239, 2, 12, 7, 0, 0, 13, 8, 0, 44, 14, 12, 1, 13, 46, 14, 0, 12, 2, 0, 11, 13, 0, 12, 55, 5, 5, 13, 1, 4, 229, 55, 1, 6, 3737560405, 1, 7, 3548103992, 39, 239, 55, 1, 6, 3218162240, 1, 7, 1066059315, 39, 239, 0, 12, 9, 0, 8, 13, 3, 12, 5, 5, 13, 1, 4, 231, 55, 1, 6, 4246922370, 1, 7, 3708039513, 39, 239, 55, 1, 4, 233, 1, 6, 1315786629, 1, 7, 1206557658, 39, 239, 2, 12, 0, 0, 0, 13, 4, 0, 0, 14, 10, 0, 9, 12, 13, 14, 58, 4, 46, 5, 45, 5, 45, 5, 0, 12, 5, 0, 45, 12, 42, 6, 8, 2, 6, 7, 0, 9, 11, 0, 8, 6, 6, 9, 58, 6, 4, 3, 0, 4, 12, 0, 21, 5, 0, 4, 18, 4, 0, 5, 0, 5, 6, 0, 21, 6, 4, 5, 5, 0, 6, 2, 4, 13, 0, 0, 5, 14, 0, 8, 6, 4, 5, 0, 5, 15, 0, 43, 7, 4, 6, 2, 0, 5, 0, 4, 6, 0, 21, 5, 7, 4, 5, 0, 5, 35, 4, 0, 0, 5, 6, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 16, 0, 11, 5, 0, 4, 0, 4, 6, 0, 21, 6, 5, 4, 5, 0, 6, 0, 4, 17, 0, 12, 5, 0, 4, 0, 4, 6, 0, 21, 6, 5, 4, 5, 0, 6, 11, 4, 0, 1, 0, 5, 6, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 11, 0, 49, 5, 1, 4, 0, 45, 5, 0, 4, 5, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/qualification.json new file mode 100644 index 00000000..ea3506ad --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/qualification.json @@ -0,0 +1,69 @@ +{ + "id": "exceptions--dispatcher--28", + "case": "exceptions", + "profile": "dispatcher", + "seed": 28, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": true, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-finally/encoded.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-finally/encoded.js new file mode 100644 index 00000000..74cf0faa --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-finally/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */4, /* 4 */undefined, /* 5 */1, /* 6 */"w0ze", /* 7 */27, /* 8 */"Math", /* 9 */"imul", /* 10 */1840398735, /* 11 */1219511517, /* 12 */352380081]; +var BYTECODE = [4, 1, 47, 4, 53, 1, 12, 1, 0, 1, 2, 5, 3, 4, 0, 4, 0, 0, 5, 2, 4, 2, 4, 1, 0, 0, 5, 2, 0, 0, 6, 3, 0, 42, 7, 3, 1, 6, 48, 6, 2, 7, 2, 9, 4, 5, 6, 0, 4, 4, 0, 45, 4, 47, 7, 147, 2, 8, 0, 0, 4, 2, 59, 102, 3, 4, 121, 0, 9, 5, 0, 11, 10, 0, 9, 5, 4, 10, 1, 3, 123, 55, 1, 5, 3525132954, 1, 6, 1435190715, 39, 131, 55, 1, 3, 125, 1, 5, 1820943250, 1, 6, 1189651724, 39, 131, 3, 9, 0, 5, 10, 9, 0, 10, 5, 0, 11, 11, 9, 10, 7, 0, 11, 58, 3, 46, 4, 45, 4, 0, 9, 4, 0, 45, 9, 42, 5, 7, 2, 5, 6, 0, 8, 6, 0, 8, 5, 5, 8, 58, 5, 4, 3, 0, 4, 7, 0, 21, 5, 0, 4, 18, 4, 0, 5, 0, 5, 0, 0, 21, 6, 4, 5, 5, 0, 6, 2, 4, 8, 0, 0, 5, 9, 0, 8, 6, 4, 5, 0, 5, 10, 0, 43, 7, 4, 6, 2, 0, 5, 0, 4, 0, 0, 21, 5, 7, 4, 5, 0, 5, 35, 4, 0, 0, 5, 0, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 11, 0, 11, 5, 0, 4, 0, 4, 0, 0, 21, 6, 5, 4, 5, 0, 6, 0, 4, 12, 0, 12, 5, 0, 4, 0, 4, 0, 0, 21, 6, 5, 4, 5, 0, 6, 11, 4, 0, 1, 0, 5, 0, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 6, 0, 49, 5, 1, 4, 0, 45, 5, 0, 4, 4, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-finally/qualification.json b/test/vm/jsconfuser-vm/fixtures/dispatcher-finally/qualification.json new file mode 100644 index 00000000..2dae9cea --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-finally/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "tryFinally--dispatcher--28", + "case": "tryFinally", + "profile": "dispatcher", + "seed": 28, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": true, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-finally/source.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-finally/source.js new file mode 100644 index 00000000..747d80bf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-finally/source.js @@ -0,0 +1 @@ +let count = 0; function check(value) { try { return value + 1; } finally { count++; } } window.TEST_OUTPUT = [check(4), count]; diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/encoded.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/encoded.js new file mode 100644 index 00000000..c8e573ed --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */4, /* 2 */2, /* 3 */1, /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */undefined, /* 7 */"p0zm", /* 8 */24, /* 9 */3199429736, /* 10 */"Math", /* 11 */"imul", /* 12 */32013595, /* 13 */1221057230, /* 14 */2674199184]; +var BYTECODE = [47, 6, 178, 2, 8, 0, 0, 4, 1, 0, 10, 0, 0, 5, 2, 10, 0, 10, 0, 0, 5, 3, 10, 0, 10, 1, 0, 22, 11, 3, 10, 34, 8, 11, 33, 8, 8, 32, 8, 8, 1, 4, 4038573869, 1, 9, 1438553269, 12, 9, 9, 4, 16, 9, 9, 8, 11, 4, 4, 9, 1, 5, 3820897902, 39, 162, 0, 10, 2, 0, 15, 11, 3, 10, 34, 8, 11, 33, 8, 8, 1, 4, 1713714976, 1, 9, 3777395586, 12, 9, 9, 4, 13, 9, 8, 9, 11, 4, 4, 9, 1, 5, 986925764, 39, 162, 11, 10, 2, 3, 5, 2, 10, 1, 4, 4065227188, 1, 5, 598037606, 39, 162, 12, 10, 2, 3, 5, 2, 10, 5, 10, 3, 0, 10, 3, 0, 11, 11, 3, 10, 5, 3, 11, 1, 4, 2626201413, 1, 5, 753394554, 39, 162, 2, 10, 4, 0, 0, 11, 5, 0, 9, 10, 11, 2, 0, 10, 6, 0, 45, 10, 42, 4, 6, 2, 4, 5, 0, 7, 7, 0, 8, 4, 4, 7, 58, 4, 4, 3, 35, 4, 0, 0, 5, 0, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 8, 0, 21, 5, 0, 4, 18, 4, 0, 5, 0, 5, 0, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 9, 0, 11, 5, 0, 4, 0, 4, 0, 0, 21, 6, 5, 4, 5, 0, 6, 11, 4, 0, 1, 0, 5, 0, 0, 21, 6, 4, 5, 5, 0, 6, 2, 4, 10, 0, 0, 5, 11, 0, 8, 6, 4, 5, 0, 5, 12, 0, 43, 7, 4, 6, 2, 0, 5, 0, 4, 0, 0, 21, 5, 7, 4, 5, 0, 5, 0, 4, 13, 0, 11, 5, 0, 4, 0, 4, 0, 0, 21, 6, 5, 4, 5, 0, 6, 0, 4, 14, 0, 11, 5, 0, 4, 0, 4, 0, 0, 21, 6, 5, 4, 5, 0, 6, 0, 4, 7, 0, 49, 5, 1, 4, 0, 45, 5, 0, 4, 6, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/qualification.json b/test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/qualification.json new file mode 100644 index 00000000..f27c2bfd --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "branches--dispatcher--35", + "case": "branches", + "profile": "dispatcher", + "seed": 35, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": true, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 2 + }, + "encodedObservation": { + "kind": "value", + "value": 2 + }, + "decodedObservation": { + "kind": "value", + "value": 2 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/source.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/source.js new file mode 100644 index 00000000..a2f57916 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-object-result/source.js @@ -0,0 +1 @@ +let value = 0; for (let i = 0; i < 4; i++) { if (i % 2) value += i; else value -= i; } window.TEST_OUTPUT = value; diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/encoded.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/encoded.js new file mode 100644 index 00000000..929932ca --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */0, /* 1 */4, /* 2 */2, /* 3 */1, /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */undefined, /* 7 */16, /* 8 */"Math", /* 9 */"imul", /* 10 */2365535147, /* 11 */3517249074, /* 12 */27]; +var BYTECODE = [47, 7, 183, 3, 10, 0, 0, 4, 1, 0, 10, 0, 0, 5, 2, 10, 0, 10, 0, 0, 5, 3, 10, 0, 10, 1, 0, 22, 11, 3, 10, 34, 8, 11, 33, 8, 8, 32, 8, 8, 1, 4, 3453634925, 1, 9, 1665060987, 12, 9, 9, 4, 16, 9, 9, 8, 11, 4, 4, 9, 1, 5, 13221, 1, 6, 5282, 39, 174, 0, 10, 2, 0, 15, 11, 3, 10, 34, 8, 11, 33, 8, 8, 1, 4, 1295132331, 1, 9, 3514397921, 12, 9, 9, 4, 13, 9, 9, 8, 11, 4, 4, 9, 1, 5, 4451, 1, 6, 15132, 39, 174, 11, 10, 2, 3, 5, 2, 10, 1, 4, 3652587690, 1, 5, 63271, 1, 6, 36447, 39, 174, 12, 10, 2, 3, 5, 2, 10, 5, 10, 3, 0, 10, 3, 0, 11, 11, 3, 10, 5, 3, 11, 1, 4, 4290077236, 1, 5, 397, 1, 6, 40736, 39, 174, 2, 10, 4, 0, 0, 11, 5, 0, 9, 10, 11, 2, 0, 10, 6, 0, 45, 10, 42, 4, 7, 3, 4, 5, 6, 58, 4, 4, 4, 0, 6, 7, 0, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 0, 0, 21, 8, 6, 7, 5, 5, 8, 2, 6, 8, 0, 0, 7, 9, 0, 8, 8, 6, 7, 0, 7, 10, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 0, 0, 21, 7, 9, 6, 5, 0, 7, 11, 6, 0, 5, 0, 7, 0, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 11, 0, 12, 7, 0, 6, 0, 6, 0, 0, 21, 8, 7, 6, 5, 0, 8, 0, 6, 12, 0, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 0, 0, 21, 8, 6, 7, 5, 0, 8, 45, 0, 0, 6, 6, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/qualification.json b/test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/qualification.json new file mode 100644 index 00000000..53b43e4a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "branches--dispatcher--33", + "case": "branches", + "profile": "dispatcher", + "seed": 33, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": true, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 2 + }, + "encodedObservation": { + "kind": "value", + "value": 2 + }, + "decodedObservation": { + "kind": "value", + "value": 2 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/source.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/source.js new file mode 100644 index 00000000..a2f57916 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-split-key/source.js @@ -0,0 +1 @@ +let value = 0; for (let i = 0; i < 4; i++) { if (i % 2) value += i; else value -= i; } window.TEST_OUTPUT = value; diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-switch/encoded.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-switch/encoded.js new file mode 100644 index 00000000..8c683d4e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-switch/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */3, /* 4 */undefined, /* 5 */2, /* 6 */"one", /* 7 */"two", /* 8 */"other", /* 9 */1485867678, /* 10 */0, /* 11 */"Math", /* 12 */"imul", /* 13 */904571761, /* 14 */18]; +var BYTECODE = [4, 1, 47, 3, 53, 1, 11, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 42, 6, 2, 1, 5, 0, 5, 3, 0, 42, 7, 2, 1, 5, 48, 5, 2, 6, 7, 9, 3, 4, 5, 0, 3, 4, 0, 45, 3, 47, 5, 202, 2, 8, 0, 0, 4, 2, 0, 9, 2, 0, 26, 10, 0, 9, 34, 7, 10, 33, 7, 7, 1, 3, 2483618958, 1, 8, 2858144006, 12, 8, 8, 3, 13, 8, 8, 7, 11, 3, 3, 8, 1, 4, 3965302847, 39, 187, 1, 3, 3720622849, 1, 4, 1178150604, 39, 187, 0, 9, 5, 0, 26, 10, 0, 9, 34, 7, 10, 33, 7, 7, 32, 7, 7, 1, 3, 2910897324, 1, 8, 3092535588, 12, 8, 8, 3, 16, 8, 7, 8, 11, 3, 3, 8, 1, 4, 2459110665, 39, 187, 1, 3, 3733390211, 1, 4, 368202392, 39, 187, 1, 3, 1855103363, 1, 4, 310194750, 39, 187, 0, 9, 6, 0, 45, 9, 0, 9, 7, 0, 45, 9, 0, 9, 8, 0, 45, 9, 0, 9, 4, 0, 45, 9, 42, 3, 5, 2, 3, 4, 1, 6, 0, 8, 3, 3, 6, 58, 3, 4, 3, 0, 4, 9, 0, 11, 5, 0, 4, 0, 4, 10, 0, 21, 6, 5, 4, 5, 0, 6, 18, 4, 0, 1, 0, 5, 10, 0, 21, 6, 4, 5, 5, 0, 6, 2, 4, 11, 0, 0, 5, 12, 0, 8, 6, 4, 5, 0, 5, 13, 0, 43, 7, 4, 6, 2, 0, 5, 0, 4, 10, 0, 21, 5, 7, 4, 5, 0, 5, 0, 4, 14, 0, 21, 5, 0, 4, 18, 4, 0, 5, 0, 5, 10, 0, 21, 6, 4, 5, 5, 0, 6, 48, 4, 1, 0, 45, 4, 0, 4, 4, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-switch/qualification.json b/test/vm/jsconfuser-vm/fixtures/dispatcher-switch/qualification.json new file mode 100644 index 00000000..a7cf6496 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-switch/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "switch--dispatcher--29", + "case": "switch", + "profile": "dispatcher", + "seed": 29, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": true, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/dispatcher-switch/source.js b/test/vm/jsconfuser-vm/fixtures/dispatcher-switch/source.js new file mode 100644 index 00000000..98a67db9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/dispatcher-switch/source.js @@ -0,0 +1 @@ +function classify(value) { switch (value) { case 1: return "one"; case 2: return "two"; default: return "other"; } } window.TEST_OUTPUT = [classify(1), classify(3)]; diff --git a/test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/encoded.js new file mode 100644 index 00000000..509325c4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/encoded.js @@ -0,0 +1,2460 @@ +var CONSTANTS = [/* 0 */1817343133, /* 1 */"kCWPnDJzpoc2A6uMfjWdpQ==", /* 2 */-654638447, /* 3 */"3IgTcKTpPCCIuDAwflhcwHJIm9EKKK2gKxi5cyAIiIAZCJixxxjg4NN7HNCASChAlclSUIH44GPep9sw54iYAet5zvC7mLwnLajEULfJ3EHZ2Jiw/Tfeo9po+/CEGVeBqzgPl43rI3+f6PNRyDmywDBHufMDb4UgNLm+MRaZuAB8i0ofe6srIetZWfGQeCXDjk96s2LZcmFQ+UKRbwtSv0ILcZdCeV/hQ1kps1zPAkNmx2txStkkIaaqOx+3ix0HiJpvMVe5ZSFm7+JzbNeBQgs5rNE6ab3+D2uVl+g68o7LGRpxiO4zo4wnDrOIXv3BnUn93uaq3ifUm8hu4frqAcCO/DLxl9nj7X750txJ5sH8yjxWh/sQbo4aFra3CZ6C/Abm8xoetqJgJp9RDcqiRn3alE8zugEmc2px8oOWLAKYvzYShJYFYrTpQ1ZMunPOdFpo9jBqNd4+JkiyQR4zglsGZ5JQLnimSVpd7nN6DsalShc+tdVtYqn+RBJ9lgACNY6VFX96u+4BWqc3Icr3XgPK13XW3uCi5icskocOM4K1BQCOyDryp+fr9n6HyvtFs7360s1WzeOwbtYSt7XqDeaa0Hbm6/0f26oWNZJdH82VRjTTmy/xookmxG1l/a2GJQuHv3MrvuYnfbDtDlV7w3jPH1OI9jJt2Z0PNZSKAh9Ri3b3BZ1ULUalcFVjz25DcsdPTVU9SaVYZV7/YQsOh0Q7KY2pFQdlhuxOU8o3EsNiXQ/1A3Wk3BKrtTd5m5gMaoWUFQytlzw4p8rT8HzExKJFF73SLDZnkfteb+kUn4WFDfSc43TY46lf4rTXNOFd6Mz0dNvbyS+JpNQU/43i/dyEChSivw8kvuQXfJzNwETexOfPPVSH5B1suJ0uNIGEYRyWc0f0fJxDLAW0vVQ5zIJ7OtTUrGk8fqRiFGb8XwxbhGA8Xqxp5EFkUOxpXE7EacxdXB70e2RDHE2suzdYnNYMX/SJFRWsazxZVHvMgnwIxKdVFby3LC1khPsKjJ0E5oX4DdScYWS04x1cirQhxL9d98y8dMvbyi//tNoUq43h/dGU+xO0v78k8tS7fdnN3kT8NPKvMFur5Cltj50FBeuEPR/kc+rk1lznLSG19EQSz5Z7Lte3rHU9lZVlFWL/eQMTl4A8Na2j5RV1mcx8WzvHQMt8XWvlbWVyHUGzLgdNgzkNafVTZV2tATtTV2zLUX8S1dJVCL2yIjmXnvMaj2EFArUGLemaDme/6zhf/cobxaBdY+3ZZuDj1y/Nsm4Fz40O/aaWLyuOv9Uq3dbQfYvN2VbvMs6v52P09swN+J38BqaK3R/Oa97m0FrZzRzGjkINzsJzc+aUosQ9xZXUCn3+iwsClpM6Ma791QZyjc4TQlC2fDJrXo/lHmq/HgGKhQcHmktuJfpFYgeuBTJQRlTCV35M0okpa75iKieWS/pBjlAaarZhLnDiA3aZ8zJe3cIxtr9RfOprZl+aAQ6xugEGpIlI8v9mKyKfvxhS9cb7QZrJolYkOr2vKRqd9gkK0oHFNuCyxR/CY7j21VLAyfLB71q878578+bJqtgx5InlArT+yBMvp6AiMamc0Qh5n87nW9G3nioGYb3pGnG5Hi6DrzcMk6NuPuF9WWLRRjuzR1HLi34o6YkhCLlsEzyHBQN2j1URWblzIW3ZVXdhy3lfdMlHsX9ZQOF9d0aTcw+IgAwZsZEo+d9gMitZv2NLWsG8eQ3Rjlg0I7GvfROr+BIBopnSMJWo1N/Ta7TwBlmJwT45n1Cq7+9j+BjCkPc53oH0CNXwtWv2p+k4z1Gs2fZA3Mj0U8On5CDtmKLxGoihAAKIgzdhmNlg3fnuUeLIOsD9VxnQtXgM4LApN7CwCHyAT/hbkDIY66E2KMrQKkiNw09YJjB/qF4gXOg/cEqYbgBIuEYQQmg84CtoetBnuGBAesgpcDXov6AGOLOvCBi9gBoIcpALKEegqd8ccInICkDEyQkw6FgQ4Ip7mhDTiNsBGYjcEBT4gWMiqK4w10nnwfp4j/DdW8un5CipkOQJ4YDPGK2z3Tf9oNto2/HPWa/AEruwVxDrqGAZ6YkRarnHAIyHC/OfaDchtLlyMZ7YF0eZyxpfGilDoQ8ZUvESd86DBw+wsBsZj2Fk+WdXRMtKvyFLLdFLeUvhepl1M3uPMQN0+VVxCZlxIXurat8WS9bGPlncsRo55KJX7x5zDRd/gZA5I5GqajL/qmsJ16s6PUH+2cty5+7SIxan0jI0maoBJYm2Htqrmifl2txu7/nxQczO57KLV8ziwH7VkdcJ6r7aCtuX/vvTbg/ag7EGTsTCA0a58+RenSKDqT4e7oovBvmbOw6bungmvGlk8nlWVcJrPpBSHNaCYSXqypYiOliOQxoD9ktqA6J9JleyT95MQpzGXlJGqkgmfZoC7lN6WgYsipE9VJaaYlX+uFMI1qBCfU5H1qJ6Pu6sGyC2rwo3nv8FJ3KHHiCiwyfd0tpOmMUOWpXOM7rJJxDq136hlfwF2L7HEtOX2eOyburSvLXmTt3ax3eF69Ne3yratdwdk4IYB6+TWS+TogYl6m3T+ttGoMswP7qqHOWYfTHtkZZwA4GPfhNp9QNtBN2uNhBL098aS6L2R11PrUElBJNYDz+DUwdDjTg9d6V1azD/Mktcxxi9TE0c1X1lb++5E1a3jQwhnZ31FHUaH2GrWzeP3HRN8sU6Va4sNaOeJyqTq3z5hcYF+7yQLJVnC/uObBPVhkXJTMTc9HeA48CcvzTItdcd+owGdPubxCz0pNrV403r/MdE/NMUPNKkMeS8fQEM4oRiBP2PjRTk5IptYdz/NA==", /* 4 */"SMItRq+yOi6jZTD6", /* 5 */-1488202197, /* 6 */"M7uNRxLL4HCv6S2hhTgvkw==", /* 7 */313509403, /* 8 */undefined, /* 9 */"kT9kq50Q", /* 10 */2003283263, /* 11 */-1074016207, /* 12 */-1416854226, /* 13 */-1245515250, /* 14 */-730807381, /* 15 */1510215120, /* 16 */941317635, /* 17 */"G0y2x0Ncpiw=", /* 18 */"/NL7Tp5BUdmSbiT7rmcKK8u+2Ak=", /* 19 */"uygzoaRpKPGKWA==", /* 20 */-1570302429, /* 21 */"Ec+8UyI3gaITGQ==", /* 22 */"hWhPIbS5DzGW2HdH", /* 23 */"NafTLCadlHU=", /* 24 */"193/beL235LxD8CD", /* 25 */1005805212, /* 26 */"jUot1rd9J+KrFgmy+Q8vgrn2NHI=", /* 27 */"OxGcvhkDhocMc4Du", /* 28 */-551848040, /* 29 */"gVNxz25FrMU=", /* 30 */351424989, /* 31 */"AZ+4NCCl", /* 32 */"yr750uJFq/2+SgPWpjsDro0a", /* 33 */"Z85BsmgmXO4=", /* 34 */"9YrqFsC57iE=", /* 35 */62683848, /* 36 */"OhaCfYAC/obMM+qviGJT5g==", /* 37 */"OIShCxGcbXRi5A==", /* 38 */"Z21enX81YoM=", /* 39 */"lL1DVXDr", /* 40 */1240281126, /* 41 */"v/3bDcWG", /* 42 */"qWw9/Ydl", /* 43 */1412632904, /* 44 */1022788467, /* 45 */"kwQ3j7fzAZdoLVO9CVU=", /* 46 */"30nOwfNI5tCovw==", /* 47 */"yVXfxb7OJVuF9xpr", /* 48 */"d9rOtyEqmd52arP1", /* 49 */1481587888, /* 50 */"ASGPWILvAXu4wC5JlLERKQ==", /* 51 */"7O8NUqTG9cEvWuT2J2o=", /* 52 */1845852839, /* 53 */"LgOVlx4Tf/0=", /* 54 */-103704984, /* 55 */"fSN312BTKv2pRQTVg7x6K3vnOXs=", /* 56 */"FCKttmES5YnGBomagS8=", /* 57 */-1655026998, /* 58 */"o0EG1r1SHD6Eoy8WsvK3CSKGiDo=", /* 59 */728783788, /* 60 */"5QT0ha4d8uzqZA==", /* 61 */"MIU8BWSPZRNrp0MjUt11dRfFU10=", /* 62 */"y8fYc/ve7KI=", /* 63 */-2117980063, /* 64 */"/BSmvOYDq4egfBPtxyU=", /* 65 */"kf/ta7omHbm5ESmZ", /* 66 */"kjx7S0HXeHtX7Q==", /* 67 */"OiZcsm0ePoJGBkySPi64pQ==", /* 68 */-1413115842, /* 69 */"zdElSZjA", /* 70 */"MUU7zQuzACdm439/I5U=", /* 71 */"lbs4zk9aY8Z1TmfSQSZpog==", /* 72 */-2014492568, /* 73 */"Gcq6XTHmlGIWHg==", /* 74 */"IheBeh7htFkTMVDP", /* 75 */707410002, /* 76 */1168820694, /* 77 */"IC2HVjH7OE9x21o1Sq1y3UZla/M=", /* 78 */"a9SdfA7su5VYNLeM7APb9+Ns6pw=", /* 79 */"dO5relsWgI0/AqGGEhI=", /* 80 */-2126881162, /* 81 */"WUVxT1zb", /* 82 */"UaqvHhmKhgc=", /* 83 */1628736062, /* 84 */"cgI2frYTcqa/IjCu", /* 85 */-1048156143, /* 86 */"V251Jga6eE512kJG", /* 87 */"7xHvisEGxJsADrT692H/qf4yjVY=", /* 88 */"KF526jFydt5MQmq2cdI=", /* 89 */"3C6LtdQFzo4=", /* 90 */129951837, /* 91 */"jT6ygqYX5nP77sih5znOzsha", /* 92 */"d/NXF3+hIDlSUVzJN39kyw==", /* 93 */"we4WUqXHFsI=", /* 94 */"od0YNau0M9//S9H04VzLrfgl", /* 95 */1110943392, /* 96 */"83/6k/oG+YLmCb2R1vo8bw==", /* 97 */"Kmi58xQYgoA=", /* 98 */-2049142363, /* 99 */1611461620, /* 100 */"+gbAk8MvyaLxJs/tCn3PxtJL", /* 101 */"9m+A5A2Vlw1lvcME", /* 102 */"JhiPgM8L25+R6OlghNk=", /* 103 */1159814596, /* 104 */"lzUdTbLc", /* 105 */"yvZQYpqvLzKlVgfClHkk1s4q6b8=", /* 106 */278687382, /* 107 */1055024927, /* 108 */"g3n0wvpOhjM4p5hiN/k=", /* 109 */"PShesEoYQIB1eGaQZih+oGfYd1A=", /* 110 */"lephfqGJ", /* 111 */"VZd1A22PWxMTeaHhLFlMNxrLbl8=", /* 112 */"LXqyltor0L8=", /* 113 */"RHx/1GgkeLx+LA==", /* 114 */true, /* 115 */-1460732367, /* 116 */-1080813574, /* 117 */"YOxndHKUcwxWvHYUV5Rk/A==", /* 118 */-1340464924, /* 119 */-395673141, /* 120 */1804160341, /* 121 */"Ls5AVnn6VG4=", /* 122 */584045222, /* 123 */"nXF1CfmADAensw==", /* 124 */"zEdCOkDO", /* 125 */2141396509, /* 126 */"9rXmHu5qxOf9W8nOS8XrRQ==", /* 127 */-1926374653, /* 128 */"fTeGqCphm+kzcbiYRgN3hw==", /* 129 */"U4tMH1uFdXVNnSMjQKc=", /* 130 */"3H4WiqMXN7o=", /* 131 */"uVgS4ONpApCKCA==", /* 132 */520937407, /* 133 */"4fbPav8dIqWaMivfxkMSx71S", /* 134 */"MLmb0S9J", /* 135 */-97413078, /* 136 */"umQm9bWdXA3hhBUbQu8=", /* 137 */304314087, /* 138 */"WukVk1g3V4M=", /* 139 */"kObsY8gfk7LBBtKBAwqG9j8a", /* 140 */"3mHGmZMw", /* 141 */"CICuGJSz", /* 142 */-1870141334, /* 143 */"j1Ek/ptCNNalM9yuguLxeTUO", /* 144 */"P3vS1zmriiAzqdoRzPihAA==", /* 145 */56918010, /* 146 */"mHUS1ZEuDruNF2CLWf0=", /* 147 */243601571, /* 148 */"Muo5Vjm6Vs5JWlv2Y267okgm", /* 149 */"+ejkUPLL/MfzWLXw53mj4dGoFTM=", /* 150 */"64rhEdO5siI=", /* 151 */"ysbhctHfuiPipvwS5I32Ba+aHg8=", /* 152 */-793021356, /* 153 */"yTVfqrDeaWo=", /* 154 */"Sd5+SnpGa9p0clnmI2KBLyGyngY=", /* 155 */"Gx9Ug5II", /* 156 */340490050, /* 157 */"srIYGaOBInm4lg==", /* 158 */"IbBfCFmAWfhdkGoodaBrKFDQB3g=", /* 159 */"X79bC1KHlfw+jQ==", /* 160 */1624753529, /* 161 */"fQ52skcanu4NernXMUpxPg==", /* 162 */"fKKR7kNyvNkFTg==", /* 163 */-1358740321, /* 164 */"UHxslDoMurwhBA==", /* 165 */-770149203, /* 166 */"/uonZqGZfDKDthUC", /* 167 */"bCkEsVVZeMEre7TXFKs=", /* 168 */"GaCROTLQvkg2wA==", /* 169 */"nT7zipoF1P2fEhaX", /* 170 */817990555, /* 171 */"lykrsYQYmo84ew==", /* 172 */"OBpWgkR2OpK7LxKip9Y=", /* 173 */"yLSLDQecnnQp9A==", /* 174 */-1107869349, /* 175 */"OXO2n0QD57QcHUJtSuUKW2vPcrs=", /* 176 */"LruKLBploeVYbQ==", /* 177 */"YMhbwEJISNASeDrgnilPsA==", /* 178 */"jnxDEyyncSNqrUZVQsVhTVXb", /* 179 */1242789084, /* 180 */"AI0KHXC1FyuTnyVrg/YNXYTN", /* 181 */"9OLEfycSqoY=", /* 182 */"QYqFBhialS0vpkkin28F8g==", /* 183 */-920704623, /* 184 */"BjCdqCtgi+k=", /* 185 */"VmtDJ0W5ZlEo2UxD", /* 186 */-1659448535, /* 187 */1802452259, /* 188 */"VJklAUCHhBNd721hUNlrUXe5", /* 189 */"2Jncbob621fQy9K+/Uo=", /* 190 */"lRXbavT+5mv81w==", /* 191 */"q+ovZ6ObMw+GuhUFq40x/ZBmxSM=", /* 192 */"M4mnMSupnmY++79P", /* 193 */-1903488502, /* 194 */-49139595, /* 195 */"D62/3TV1lcQvX6Gz", /* 196 */"6qXxJele4fLaR/7Dkk4CsrYl+m78+s6X", /* 197 */"MJTfDFa8", /* 198 */"5SnCovDW91P6/7FC", /* 199 */"Me6CZfiSoQ72g7wHxZLS/uVl3yU=", /* 200 */"ZDp8zlRaVPY=", /* 201 */"", /* 202 */-193595131, /* 203 */"DyvtvyUruJZffUON", /* 204 */-790804353, /* 205 */1468521095, /* 206 */-728235884, /* 207 */-1136900335, /* 208 */-1256908166, /* 209 */-1780145097, /* 210 */-1856328047, /* 211 */"qXUG5KZcPiyAtz1c9swldLHVtKw=", /* 212 */2091902540, /* 213 */"qIASCLZzdBiroBIoh9EMSA==", /* 214 */"vpEdeNaAVhjcs1koemBb+A==", /* 215 */"cZ/56yR3kdwFTbVFcdU=", /* 216 */883112833, /* 217 */"lS3atoFakM/me5bWN63dPQCV", /* 218 */"bdTUv4lEH9Q=", /* 219 */1348232663, /* 220 */"77GLKP3goX9sk6cIAoE=", /* 221 */-1509263095, /* 222 */"ZApvllR6Ve4xWnWyMM4=", /* 223 */"HWiakQkIMYDzGypw+PglYIYpObA=", /* 224 */"hLX5Hs2C+fczkpgud6LHVQ==", /* 225 */"Fh/ton4pUNEUSUXHeFtUL6y6TCE=", /* 226 */-1617983790, /* 227 */"27zI1cNF4fxhS+XX", /* 228 */"CDGN2eZg+PjHF8iL", /* 229 */"UsOmdzbTn64=", /* 230 */-1384399246, /* 231 */"EOrsXRLGxMInXrHzM2ad4g==", /* 232 */"pUVJTcXaBXeZ6zif", /* 233 */"j0abypt/aeqsJgq6yBFahg==", /* 234 */"1Yf7AN+IgxEd+Q==", /* 235 */725691163, /* 236 */"4ubwqskx", /* 237 */"sTgokKpv", /* 238 */1270594118, /* 239 */-843652785, /* 240 */"+PUlVQEvr7s=", /* 241 */"tLgOILtpCvCsSDbAXThxUA==", /* 242 */"ExAMuJEgV9g=", /* 243 */-2039249381, /* 244 */"wEfJy6FQESi9ocwYzPA=", /* 245 */"Os+WQG75", /* 246 */1037198951, /* 247 */167804901, /* 248 */"POyXXHjEZMx/XED0A2RPHA==", /* 249 */"FjreoTDui3KYB/qD8j7nkvxp", /* 250 */"6TPIhi4N3f0sFcqq1z/zqw==", /* 251 */-2108083531, /* 252 */"y0rmxvO7H06/2gtm", /* 253 */"3c0XRe3E6E8=", /* 254 */"Eduwbyb7ipSTDQ==", /* 255 */-121659733, /* 256 */"3JPIeMbh9lnisNfI+F8=", /* 257 */"qMEgSb/yAGc=", /* 258 */"DuWRegTPglstx5vKAF3t5Q==", /* 259 */"VAdemxfvEXmu4TqpoDA=", /* 260 */-499634952, /* 261 */"8f3+bdTWr7o=", /* 262 */"heBFeXLRSklkQ23HbVM=", /* 263 */"yqM0LqZSHfa+QQ==", /* 264 */289211467, /* 265 */"uj4py71H4PrhTu/V/KWfLg==", /* 266 */"xjbFmuAPp/vyFg==", /* 267 */-2022177370, /* 268 */-143409730, /* 269 */"Gp7javH1", /* 270 */"tOhxYN7rplBiuI7B", /* 271 */"SBkLsX85QqEjbw==", /* 272 */"hMZgSo/dLLaGIiyelnNX9uUC", /* 273 */-1927093016, /* 274 */"yUbc+v9RoNnLotIu2JP1Bg==", /* 275 */"hkcy3K1sxuX6Zeasyjw=", /* 276 */"Gmat7QgSpYZdAgCe", /* 277 */-1537613317, /* 278 */"uNjbYcrpvRDhiNYHvpvu8NR45OE=", /* 279 */"fgB/mHMQeHhj4GOonTBOyG1ANPg=", /* 280 */"NK1IN1ab", /* 281 */1942547631, /* 282 */"aM4nsrJWNeLBeVSW", /* 283 */"UoxBBDqE", /* 284 */"RxVRhYwMLpOedxXj", /* 285 */"au9jZWoVNo1FPSaH", /* 286 */-188040089, /* 287 */"z1Ub1Jw8EUyG1w==", /* 288 */"ZSRXtAUMQIRYBIGNGhyWpCQk", /* 289 */996258490, /* 290 */1742996334, /* 291 */"iMraVd79/2IWpqEzBY6GAg==", /* 292 */"PcaiTR/yhFYSolY+fJJaFlSCYgo=", /* 293 */"Kqe7LDSdHgU=", /* 294 */"gGVirkk6MVY=", /* 295 */816589685, /* 296 */"2m7h8UQZqI4=", /* 297 */-1062527368, /* 298 */-1093512761, /* 299 */-514363801, /* 300 */"fCyZpFcU", /* 301 */-1448327308, /* 302 */284881309, /* 303 */"sjk60ZVYOOPhb+KQ", /* 304 */-395860092, /* 305 */"iNcvqrc+rKU=", /* 306 */"jnYHiWwBeIk=", /* 307 */1291645142, /* 308 */1768079372, /* 309 */1640212776, /* 310 */-2145909563, /* 311 */-1151273029, /* 312 */"1tL1Vp7BMco=", /* 313 */422113239, /* 314 */-887183724, /* 315 */-679921338, /* 316 */-1238467868, /* 317 */1954671843, /* 318 */1467729264, /* 319 */-1869250213, /* 320 */922162728, /* 321 */-986402154, /* 322 */946876227, /* 323 */-956441785, /* 324 */1497101511]; +var BYTECODE = [82, 83, 84, 47, 6, 3567, 1, 79, 0, 0, 85, 42, 5, 6, 1, 7, 47, 8, 6390, 4, 96, 0, 0, 86, 47, 15, 116, 1, 100, 1, 0, 1, 5, 61, 2, 13, 13, 12, 12, 13, 15, 12, 87, 88, 89, 90, 42, 3, 8, 4, 5, 9, 10, 11, 142, 5, 16, 2806765098, 62, 14, 1241678039, 14, 14, 13, 12, 12, 12, 14, 16, 12, 17, 12, 6, 42, 16, 2, 1, 17, 142, 7, 17, 313509407, 42, 18, 2, 1, 17, 91, 121, 9, 2952990233, 10, 254, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 92, 48, 17, 3, 16, 18, 19, 93, 142, 8, 15, 0, 94, 120, 95, 9, 4158068206, 98, 10, 2003283263, 97, 11, 3220930215, 96, 12, 2878090021, 47, 91, 9800, 2, 13, 0, 0, 95, 142, 13, 20, 3049393807, 61, 21, 96, 95, 95, 95, 96, 20, 95, 142, 10, 22, 2003283263, 61, 23, 96, 96, 95, 96, 96, 22, 95, 142, 14, 24, 3564134044, 63, 96, 96, 96, 21, 180, 95, 96, 24, 98, 95, 25, 96, 64, 96, 96, 96, 93, 96, 95, 25, 95, 95, 95, 95, 96, 65, 93, 15, 96, 93, 96, 95, 1510270861, 115, 89, 2692630380, 94, 1316174328, 66, 96, 95, 94, 89, 95, 96, 96, 94, 96, 96, 96, 95, 67, 95, 94, 95, 95, 95, 95, 95, 95, 96, 93, 94, 68, 95, 96, 96, 94, 96, 97, 245, 95, 96, 96, 96, 89, 89, 141, 2734408, 90, 77, 142, 16, 26, 941306846, 61, 23, 96, 96, 95, 96, 96, 26, 96, 69, 95, 3336039002, 3705966737, 95, 17, 23, 96, 96, 27, 21, 18, 96, 64, 96, 96, 96, 93, 96, 96, 27, 96, 95, 96, 96, 95, 65, 93, 19, 95, 93, 96, 96, 465988970, 115, 89, 3390261139, 94, 1306449013, 66, 95, 95, 94, 89, 95, 95, 96, 94, 96, 95, 96, 95, 67, 96, 94, 96, 95, 95, 95, 95, 96, 95, 93, 94, 68, 95, 95, 95, 94, 95, 97, 340, 95, 96, 95, 95, 89, 89, 141, 3352304486, 90, 77, 115, 89, 2588879981, 90, 16508045, 77, 142, 20, 28, 2724664677, 66, 96, 96, 29, 28, 96, 96, 96, 23, 96, 96, 96, 96, 61, 23, 95, 96, 96, 96, 96, 29, 96, 69, 95, 1415897591, 2672232803, 96, 21, 23, 96, 95, 30, 21, 22, 96, 64, 96, 95, 96, 93, 95, 95, 30, 95, 95, 96, 96, 96, 65, 93, 23, 95, 93, 96, 96, 545970102, 62, 95, 2789704886, 95, 95, 96, 95, 96, 96, 95, 93, 96, 93, 95, 24, 115, 89, 2531012884, 94, 445334518, 66, 95, 96, 94, 89, 96, 95, 96, 94, 96, 96, 95, 96, 70, 96, 96, 96, 96, 96, 96, 96, 93, 94, 95, 94, 96, 68, 96, 96, 96, 94, 96, 97, 470, 96, 96, 96, 96, 89, 89, 141, 2130326635, 90, 77, 115, 89, 3352025788, 90, 2666115836, 77, 142, 25, 31, 1005786496, 66, 95, 96, 32, 31, 96, 96, 96, 23, 95, 96, 95, 95, 61, 23, 96, 95, 96, 96, 95, 32, 95, 69, 95, 4023828742, 3696771, 95, 26, 23, 95, 96, 33, 21, 27, 96, 64, 96, 95, 96, 93, 95, 95, 33, 96, 95, 95, 95, 96, 65, 93, 28, 96, 93, 95, 95, 3743137441, 62, 95, 3510152853, 96, 95, 95, 95, 95, 96, 95, 93, 96, 93, 96, 29, 115, 89, 3973849035, 94, 1611705829, 66, 95, 96, 94, 89, 96, 95, 96, 94, 96, 95, 96, 96, 70, 96, 95, 96, 95, 96, 95, 95, 93, 94, 96, 94, 96, 68, 96, 96, 95, 94, 95, 97, 600, 95, 96, 95, 95, 89, 89, 141, 1170127810, 90, 77, 115, 89, 2984444660, 90, 133114711, 77, 142, 30, 34, 351440091, 68, 95, 95, 96, 34, 95, 97, 628, 95, 95, 96, 95, 35, 23, 61, 23, 96, 96, 95, 95, 96, 35, 95, 69, 95, 1192180661, 1930804002, 95, 31, 23, 95, 95, 36, 21, 32, 95, 64, 95, 95, 96, 93, 96, 96, 36, 95, 95, 96, 95, 96, 65, 93, 33, 96, 93, 96, 95, 1914360570, 62, 96, 2816884932, 96, 96, 96, 96, 96, 95, 96, 93, 96, 93, 95, 34, 115, 89, 1958990936, 94, 2334041658, 66, 96, 95, 94, 89, 95, 95, 95, 94, 95, 95, 96, 96, 70, 95, 96, 96, 96, 95, 95, 96, 93, 94, 96, 94, 96, 68, 95, 95, 96, 94, 96, 97, 731, 96, 96, 95, 95, 89, 89, 141, 3039351290, 90, 77, 115, 89, 760420893, 90, 1661573805, 77, 142, 35, 37, 62656425, 66, 96, 95, 38, 37, 95, 95, 96, 23, 95, 96, 95, 95, 61, 23, 96, 96, 96, 96, 96, 38, 95, 69, 96, 72583250, 2928502939, 96, 36, 23, 95, 95, 39, 21, 37, 95, 64, 95, 95, 95, 93, 95, 95, 39, 96, 96, 96, 95, 95, 65, 93, 38, 95, 93, 96, 96, 1788785158, 62, 95, 553175701, 96, 96, 96, 95, 96, 96, 95, 93, 96, 93, 95, 39, 115, 89, 4152305510, 94, 2334889216, 66, 95, 95, 94, 89, 96, 95, 95, 94, 95, 95, 96, 96, 70, 95, 95, 95, 95, 96, 96, 96, 93, 94, 96, 94, 95, 68, 96, 96, 95, 94, 96, 97, 861, 95, 95, 95, 96, 89, 89, 141, 1951957834, 90, 77, 115, 89, 1997401716, 90, 1336733749, 77, 142, 40, 40, 1240291814, 66, 95, 95, 41, 40, 95, 96, 96, 23, 96, 96, 96, 95, 61, 23, 96, 95, 96, 96, 95, 41, 96, 69, 96, 2328117405, 3585047090, 95, 41, 23, 95, 96, 42, 21, 42, 95, 64, 96, 96, 96, 93, 96, 95, 42, 96, 96, 96, 96, 96, 65, 93, 43, 96, 93, 96, 96, 1412679379, 115, 89, 209982773, 94, 2303297495, 66, 96, 95, 94, 89, 95, 96, 96, 94, 96, 95, 95, 96, 67, 95, 94, 95, 96, 95, 96, 96, 96, 95, 94, 93, 68, 95, 96, 96, 94, 95, 97, 975, 96, 95, 96, 96, 89, 89, 141, 2957314387, 90, 77, 115, 89, 1107409039, 90, 339085571, 77, 142, 44, 43, 1022803116, 68, 96, 96, 96, 43, 96, 97, 1003, 96, 95, 95, 95, 44, 23, 61, 23, 95, 95, 95, 95, 96, 44, 96, 69, 95, 1145739068, 125102896, 96, 45, 23, 95, 96, 45, 21, 46, 96, 64, 96, 96, 96, 93, 96, 96, 45, 96, 95, 96, 95, 96, 65, 93, 47, 95, 93, 95, 96, 1279364283, 62, 95, 4080326516, 95, 95, 95, 96, 96, 95, 96, 93, 95, 93, 95, 48, 115, 89, 1419345584, 94, 3612656466, 66, 95, 96, 94, 89, 95, 95, 96, 94, 96, 96, 95, 96, 70, 96, 96, 96, 95, 96, 96, 95, 94, 93, 96, 94, 96, 68, 95, 95, 96, 94, 96, 97, 1106, 96, 96, 95, 95, 89, 89, 141, 1198020955, 90, 77, 115, 89, 3728512691, 90, 3666810515, 77, 142, 49, 46, 1481581125, 68, 95, 95, 96, 46, 95, 97, 1134, 95, 95, 95, 95, 47, 23, 61, 23, 96, 96, 95, 96, 95, 47, 96, 69, 95, 2204676750, 103437081, 95, 50, 23, 95, 95, 48, 21, 51, 96, 64, 95, 96, 95, 93, 96, 95, 48, 96, 96, 95, 96, 95, 65, 93, 52, 96, 93, 95, 95, 1845837050, 62, 95, 2371544602, 96, 95, 96, 95, 96, 95, 96, 93, 96, 93, 95, 53, 115, 89, 3443683473, 94, 1241449143, 66, 96, 95, 94, 89, 95, 95, 95, 94, 96, 95, 95, 95, 70, 96, 96, 96, 95, 96, 96, 96, 93, 94, 96, 94, 95, 68, 96, 95, 95, 94, 95, 97, 1237, 95, 96, 96, 95, 89, 89, 141, 3778803989, 90, 77, 115, 89, 3800832937, 90, 191361110, 77, 142, 54, 49, 4191269235, 66, 95, 96, 50, 49, 96, 96, 95, 23, 95, 95, 95, 95, 61, 23, 95, 96, 95, 96, 95, 50, 95, 69, 95, 2874799605, 3898907873, 96, 55, 23, 95, 95, 51, 21, 56, 95, 64, 96, 95, 96, 93, 95, 95, 51, 95, 95, 95, 95, 96, 65, 93, 57, 96, 93, 96, 96, 2639931457, 62, 95, 3851150898, 95, 95, 95, 96, 95, 96, 95, 93, 96, 93, 95, 58, 115, 89, 1896330990, 94, 4138730700, 66, 96, 96, 94, 89, 96, 96, 96, 94, 96, 96, 96, 95, 70, 96, 95, 96, 95, 96, 95, 96, 94, 93, 96, 94, 96, 68, 96, 95, 96, 94, 96, 97, 1367, 95, 95, 96, 95, 89, 89, 141, 2628886240, 90, 77, 115, 89, 1809447691, 90, 2572912583, 77, 142, 59, 52, 728787941, 66, 96, 95, 53, 52, 95, 95, 96, 23, 96, 96, 96, 95, 61, 23, 96, 96, 96, 96, 95, 53, 96, 69, 95, 515598700, 330928327, 95, 60, 23, 95, 95, 54, 21, 61, 95, 64, 95, 96, 95, 93, 95, 95, 54, 95, 96, 95, 96, 96, 65, 93, 62, 96, 93, 95, 95, 2246405544, 115, 89, 2156565674, 94, 73279108, 66, 96, 95, 94, 89, 96, 96, 96, 94, 95, 96, 96, 95, 67, 95, 94, 96, 95, 96, 95, 95, 96, 95, 93, 94, 68, 95, 95, 96, 94, 96, 97, 1481, 96, 95, 96, 96, 89, 89, 141, 2814719202, 90, 77, 115, 89, 291997053, 90, 226217224, 77, 142, 63, 55, 2176985335, 66, 96, 96, 56, 55, 95, 96, 96, 23, 96, 96, 96, 95, 61, 23, 95, 95, 95, 95, 95, 56, 95, 69, 96, 5400365, 4105563920, 95, 64, 23, 96, 95, 57, 21, 65, 96, 64, 95, 96, 96, 93, 96, 95, 57, 96, 95, 95, 96, 95, 65, 93, 66, 95, 93, 96, 95, 1504053323, 62, 95, 2550472242, 95, 96, 95, 96, 96, 96, 96, 93, 95, 93, 95, 67, 115, 89, 3121610482, 94, 911503568, 66, 96, 96, 94, 89, 96, 95, 95, 94, 96, 95, 95, 96, 70, 96, 96, 96, 95, 96, 95, 96, 93, 94, 95, 94, 96, 68, 95, 95, 96, 94, 95, 97, 1611, 95, 96, 95, 95, 89, 89, 141, 1384366112, 90, 77, 115, 89, 3238180888, 90, 60659387, 77, 142, 68, 58, 2881856480, 68, 95, 96, 95, 58, 95, 97, 1639, 96, 96, 96, 96, 59, 23, 61, 23, 96, 95, 95, 95, 96, 59, 95, 69, 95, 896717549, 2482395823, 96, 69, 23, 95, 96, 60, 21, 70, 95, 64, 95, 96, 96, 93, 95, 95, 60, 95, 96, 96, 95, 96, 65, 93, 71, 95, 93, 96, 95, 3278454076, 115, 89, 2808633965, 94, 722856015, 66, 96, 96, 94, 89, 96, 96, 96, 94, 95, 96, 96, 96, 67, 96, 94, 96, 95, 95, 95, 95, 95, 96, 93, 94, 68, 95, 95, 96, 94, 95, 97, 1726, 95, 96, 95, 95, 89, 89, 141, 527531626, 90, 77, 115, 89, 1159340671, 90, 1090180950, 77, 142, 72, 61, 2280487481, 68, 95, 96, 96, 61, 95, 97, 1754, 96, 96, 95, 96, 62, 23, 61, 23, 95, 95, 96, 96, 95, 62, 95, 69, 96, 944637096, 3794770601, 96, 73, 23, 96, 95, 63, 21, 74, 96, 64, 95, 95, 95, 93, 96, 96, 63, 95, 96, 96, 95, 96, 65, 93, 75, 95, 93, 95, 95, 707405910, 115, 89, 2087657336, 94, 4028868934, 66, 96, 95, 94, 89, 95, 95, 95, 94, 96, 95, 96, 95, 67, 96, 94, 96, 95, 96, 96, 95, 95, 96, 93, 94, 68, 95, 96, 96, 94, 95, 97, 1841, 96, 96, 95, 96, 89, 89, 141, 3581801512, 90, 77, 115, 89, 2766690513, 90, 1478503342, 77, 142, 76, 64, 1168811583, 66, 95, 96, 65, 64, 95, 96, 96, 23, 95, 95, 95, 95, 61, 23, 95, 95, 96, 96, 95, 65, 96, 69, 95, 307244955, 906492909, 96, 77, 23, 96, 96, 66, 21, 78, 95, 64, 95, 96, 96, 93, 95, 96, 66, 95, 95, 96, 96, 95, 65, 93, 79, 95, 93, 95, 96, 1692892441, 115, 89, 2958542167, 94, 202142581, 66, 95, 96, 94, 89, 95, 95, 96, 94, 95, 96, 95, 96, 67, 95, 94, 96, 95, 95, 95, 95, 96, 96, 94, 93, 68, 95, 95, 95, 94, 95, 97, 1955, 95, 96, 95, 96, 89, 89, 141, 1392223965, 90, 77, 115, 89, 3401020775, 90, 3383979869, 77, 142, 80, 67, 2168123026, 68, 96, 96, 96, 67, 96, 97, 1983, 95, 95, 96, 95, 68, 23, 61, 23, 95, 96, 96, 96, 96, 68, 96, 69, 95, 40879744, 4139422781, 96, 81, 23, 96, 95, 69, 21, 82, 96, 64, 96, 96, 95, 93, 95, 96, 69, 96, 96, 95, 96, 96, 65, 93, 83, 95, 93, 96, 96, 1628705443, 62, 96, 318771656, 96, 96, 96, 96, 95, 96, 96, 93, 96, 93, 96, 84, 115, 89, 1000048436, 94, 3026192662, 66, 95, 95, 94, 89, 95, 96, 95, 94, 95, 95, 96, 96, 70, 95, 96, 95, 95, 95, 95, 96, 94, 93, 96, 94, 95, 68, 95, 96, 95, 94, 96, 97, 2086, 95, 95, 95, 95, 89, 89, 141, 38266939, 90, 77, 115, 89, 2235418379, 90, 2165065800, 77, 142, 85, 70, 3246796789, 68, 96, 95, 96, 70, 96, 97, 2114, 95, 95, 96, 95, 71, 23, 61, 23, 95, 96, 96, 95, 96, 71, 96, 69, 96, 789351034, 2969235037, 95, 86, 23, 96, 95, 72, 21, 87, 96, 64, 96, 95, 95, 93, 96, 96, 72, 96, 96, 96, 95, 96, 65, 93, 88, 95, 93, 95, 95, 2979374776, 62, 96, 1340073053, 96, 96, 96, 96, 95, 96, 96, 93, 95, 93, 96, 89, 115, 89, 2318939260, 94, 384607762, 66, 95, 96, 94, 89, 96, 95, 96, 94, 95, 95, 96, 95, 70, 95, 96, 96, 95, 96, 96, 95, 94, 93, 96, 94, 95, 68, 96, 95, 95, 94, 95, 97, 2217, 95, 95, 95, 95, 89, 89, 141, 3629966548, 90, 77, 115, 89, 1986409253, 90, 2868965857, 77, 142, 90, 73, 129894465, 66, 96, 96, 74, 73, 95, 95, 95, 23, 96, 95, 95, 96, 61, 23, 96, 95, 95, 95, 95, 74, 95, 69, 95, 1984111528, 680741814, 95, 91, 23, 95, 96, 75, 21, 92, 95, 64, 95, 95, 96, 93, 96, 96, 75, 96, 96, 96, 96, 96, 65, 93, 93, 96, 93, 96, 95, 372626440, 62, 96, 2505885600, 95, 95, 96, 95, 96, 96, 96, 93, 95, 93, 95, 94, 115, 89, 1232162892, 94, 3577536046, 66, 95, 95, 94, 89, 96, 95, 96, 94, 96, 96, 96, 95, 70, 96, 96, 95, 96, 96, 96, 95, 94, 93, 95, 94, 96, 68, 96, 95, 96, 94, 95, 97, 2347, 96, 96, 96, 95, 89, 89, 141, 1523597182, 90, 77, 115, 89, 172531474, 90, 1437804656, 77, 142, 95, 76, 1110952235, 68, 96, 95, 96, 76, 95, 97, 2375, 96, 96, 96, 96, 77, 23, 61, 23, 96, 96, 95, 95, 96, 77, 95, 69, 96, 3677633334, 1280623226, 95, 96, 23, 95, 95, 78, 21, 97, 95, 64, 95, 95, 95, 93, 95, 96, 78, 95, 95, 95, 95, 95, 65, 93, 98, 95, 93, 95, 95, 2245797349, 115, 89, 2678041806, 94, 600325676, 66, 96, 96, 94, 89, 95, 95, 95, 94, 95, 96, 95, 96, 67, 95, 94, 95, 95, 96, 95, 95, 95, 95, 93, 94, 68, 95, 95, 95, 94, 95, 97, 2462, 95, 95, 95, 95, 89, 89, 141, 2092038313, 90, 77, 115, 89, 3912638980, 90, 74363284, 77, 142, 99, 79, 1611438317, 68, 96, 96, 95, 79, 96, 97, 2490, 96, 96, 96, 96, 80, 23, 61, 23, 95, 95, 96, 96, 96, 80, 96, 69, 95, 2432435566, 2767780996, 96, 100, 23, 96, 96, 81, 21, 101, 95, 64, 95, 96, 95, 93, 95, 95, 81, 95, 95, 96, 95, 95, 65, 93, 102, 95, 93, 95, 95, 136687827, 115, 89, 706408509, 94, 2815394371, 66, 95, 96, 94, 89, 95, 95, 95, 94, 96, 95, 95, 96, 67, 95, 94, 95, 95, 96, 96, 95, 96, 96, 94, 93, 68, 96, 96, 96, 94, 95, 97, 2577, 96, 96, 96, 96, 89, 89, 141, 1895265677, 90, 77, 115, 89, 2743279986, 90, 2528597954, 77, 142, 103, 82, 1159850333, 66, 95, 96, 83, 82, 96, 96, 96, 23, 95, 96, 96, 96, 61, 23, 95, 96, 95, 95, 96, 83, 95, 69, 96, 1130151875, 670322186, 95, 104, 23, 95, 95, 84, 21, 105, 95, 64, 95, 95, 95, 93, 95, 95, 84, 95, 95, 95, 95, 95, 65, 93, 106, 96, 93, 95, 96, 278664774, 115, 89, 2727546825, 94, 778017323, 66, 96, 95, 94, 89, 96, 95, 95, 94, 96, 95, 95, 95, 67, 96, 94, 96, 95, 96, 96, 96, 95, 95, 93, 94, 68, 96, 96, 96, 94, 95, 97, 2691, 96, 95, 95, 95, 89, 89, 141, 3656645667, 90, 77, 115, 89, 3991672919, 90, 2316863391, 77, 142, 107, 85, 1055021965, 66, 95, 96, 86, 85, 95, 95, 95, 23, 95, 95, 95, 96, 61, 23, 95, 95, 96, 95, 95, 86, 95, 69, 95, 2245058526, 1878712294, 96, 108, 23, 96, 96, 87, 21, 109, 96, 64, 95, 95, 96, 93, 95, 95, 87, 95, 96, 96, 96, 96, 65, 93, 110, 95, 93, 95, 96, 3163669218, 62, 96, 1474539036, 96, 95, 96, 96, 95, 96, 95, 93, 95, 93, 95, 111, 115, 89, 3916408944, 94, 1856191126, 66, 96, 96, 94, 89, 96, 96, 96, 94, 95, 96, 96, 95, 70, 95, 96, 96, 96, 95, 95, 96, 93, 94, 95, 94, 95, 68, 95, 96, 95, 94, 95, 97, 2821, 95, 95, 95, 95, 89, 89, 141, 3165402116, 90, 77, 115, 89, 3594335090, 90, 2053040165, 77, 115, 89, 2792328129, 90, 2792427959, 77, 96, 47, 15, 6390, 4, 96, 0, 0, 97, 141, 37354, 88, 66, 96, 95, 21, 88, 96, 96, 95, 21, 95, 95, 96, 96, 115, 89, 89643997, 90, 88687019, 77, 55, 115, 4, 3026, 88, 10721, 66, 95, 95, 21, 88, 95, 95, 95, 21, 95, 95, 95, 95, 115, 89, 3010695243, 90, 3010267709, 77, 55, 141, 17897, 88, 68, 95, 95, 95, 88, 95, 97, 2911, 96, 96, 95, 96, 21, 21, 115, 89, 2373534810, 90, 2372394540, 77, 98, 99, 118, 6, 10, 2003283263, 88, 2002, 68, 96, 96, 96, 88, 96, 97, 2939, 96, 96, 96, 96, 21, 21, 115, 89, 2911274473, 90, 2912427935, 77, 141, 21785, 21, 80, 141, 2945, 4, 55, 141, 4254, 88, 66, 95, 95, 21, 88, 95, 95, 95, 21, 96, 95, 95, 95, 115, 89, 2523503893, 90, 2524383075, 77, 81, 42, 8, 15, 4, 14, 16, 17, 18, 44, 99, 7, 1, 8, 100, 138, 21, 3130, 16, 476390256, 17, 278, 18, 7, 141, 61198, 88, 68, 96, 95, 96, 88, 95, 97, 3016, 96, 95, 96, 96, 21, 21, 115, 89, 861023143, 90, 860664273, 77, 141, 45924, 21, 80, 141, 1189, 21, 142, 8, 99, 0, 101, 71, 96, 95, 96, 95, 96, 0, 6, 96, 0, 99, 96, 96, 96, 64, 95, 96, 96, 93, 96, 96, 99, 95, 96, 96, 95, 95, 65, 93, 112, 96, 93, 95, 95, 3641133519, 115, 89, 2960781283, 94, 2673150301, 66, 96, 96, 94, 89, 95, 96, 95, 94, 95, 95, 96, 95, 67, 95, 94, 96, 95, 95, 95, 95, 95, 95, 94, 93, 68, 95, 95, 95, 94, 95, 97, 3113, 96, 95, 95, 95, 89, 89, 141, 2258252661, 90, 77, 141, 21230, 88, 66, 95, 95, 21, 88, 95, 95, 95, 21, 95, 95, 96, 96, 115, 89, 2354240908, 90, 2353809402, 77, 141, 2507, 88, 68, 95, 96, 96, 88, 95, 97, 3156, 95, 95, 96, 95, 21, 21, 115, 89, 1448372637, 90, 1447752683, 77, 102, 121, 16, 3074141391, 17, 266, 88, 24840, 66, 96, 95, 21, 88, 96, 96, 96, 21, 95, 96, 96, 95, 115, 89, 4064388790, 90, 4066327744, 77, 81, 42, 12, 15, 4, 14, 16, 17, 18, 118, 13, 114, 0, 88, 19103, 66, 96, 95, 21, 88, 95, 96, 96, 21, 95, 96, 95, 95, 115, 89, 1539092298, 90, 1537357116, 77, 141, 63688, 88, 68, 95, 96, 96, 88, 95, 97, 3240, 96, 95, 95, 96, 21, 21, 115, 89, 3491437073, 90, 3490021479, 77, 142, 5, 99, 2806765098, 68, 95, 95, 96, 99, 95, 97, 3264, 95, 95, 95, 95, 9, 0, 55, 141, 1365, 88, 68, 95, 96, 95, 88, 95, 97, 3282, 96, 95, 95, 96, 21, 21, 115, 89, 1123895297, 90, 1122226807, 77, 61, 5, 96, 95, 95, 95, 96, 9, 95, 141, 3022, 4, 55, 141, 14447, 88, 68, 95, 96, 95, 88, 96, 97, 3318, 96, 95, 96, 96, 21, 21, 115, 89, 392395799, 90, 394126945, 77, 103, 104, 141, 46520, 88, 68, 96, 96, 96, 88, 95, 97, 3343, 96, 96, 95, 96, 21, 21, 115, 89, 503187017, 90, 501259327, 77, 141, 62928, 21, 105, 42, 10, 15, 4, 14, 16, 17, 18, 106, 61, 5, 95, 95, 95, 95, 95, 99, 95, 141, 36203, 88, 66, 95, 95, 21, 88, 96, 96, 95, 21, 95, 95, 95, 95, 115, 89, 1889851958, 90, 1891254336, 77, 141, 60731, 88, 71, 95, 95, 95, 95, 96, 0, 88, 96, 21, 88, 95, 96, 96, 64, 95, 96, 96, 93, 96, 95, 88, 96, 96, 95, 96, 95, 65, 93, 115, 95, 93, 96, 95, 2834212008, 115, 89, 1598341159, 94, 5042622, 66, 96, 95, 94, 89, 95, 96, 95, 94, 96, 95, 96, 96, 67, 96, 94, 96, 96, 96, 95, 96, 96, 96, 94, 93, 68, 96, 95, 95, 94, 96, 97, 3475, 96, 96, 96, 95, 89, 89, 141, 4131251332, 90, 77, 141, 8888, 88, 68, 95, 96, 96, 88, 96, 97, 3496, 95, 95, 96, 95, 21, 21, 115, 89, 3950735239, 90, 3949327857, 77, 141, 20605, 88, 68, 96, 96, 95, 88, 95, 97, 3519, 95, 96, 95, 96, 21, 21, 115, 89, 1399386098, 90, 1400272260, 77, 141, 23871, 21, 107, 121, 16, 2952990233, 17, 254, 88, 11640, 68, 96, 96, 96, 88, 95, 97, 3550, 96, 96, 95, 96, 21, 21, 115, 89, 1300832824, 90, 1301788750, 77, 42, 89, 91, 2, 89, 90, 141, 0, 92, 108, 109, 120, 75, 116, 3214199393, 73, 117, 3431678058, 74, 118, 2954553078, 76, 10, 2003283263, 47, 69, 10067, 2, 13, 0, 0, 110, 142, 119, 16, 3899289259, 61, 17, 74, 74, 74, 74, 73, 16, 74, 142, 10, 18, 2003283263, 61, 19, 73, 73, 73, 74, 73, 18, 74, 142, 120, 20, 1804158729, 63, 73, 74, 74, 17, 3631, 74, 74, 20, 76, 74, 21, 74, 64, 74, 73, 73, 71, 74, 73, 21, 73, 74, 73, 73, 74, 65, 71, 121, 74, 71, 74, 74, 676298869, 115, 67, 1257502254, 72, 2962943133, 66, 73, 73, 72, 67, 73, 73, 73, 72, 74, 73, 73, 73, 67, 74, 72, 74, 73, 74, 73, 74, 74, 73, 71, 72, 68, 73, 74, 74, 72, 73, 75, 3696, 73, 73, 74, 74, 67, 67, 141, 931925894, 68, 79, 142, 122, 22, 584010331, 61, 19, 74, 74, 74, 74, 74, 22, 74, 69, 73, 4080369908, 1399345479, 73, 123, 19, 74, 74, 23, 17, 124, 74, 64, 73, 73, 73, 71, 74, 74, 23, 73, 74, 73, 74, 73, 65, 71, 125, 73, 71, 73, 74, 2141394267, 62, 73, 1148921461, 73, 74, 73, 73, 73, 74, 74, 71, 73, 71, 73, 126, 115, 67, 3124711129, 72, 1202436127, 66, 73, 73, 72, 67, 74, 74, 74, 72, 73, 73, 74, 73, 70, 74, 73, 74, 74, 74, 73, 73, 72, 71, 73, 72, 74, 68, 74, 73, 74, 72, 74, 75, 3807, 73, 74, 73, 74, 67, 67, 141, 1000770658, 68, 79, 115, 67, 727818863, 68, 372625839, 79, 142, 127, 24, 2368597634, 68, 73, 74, 74, 24, 74, 75, 3835, 73, 73, 73, 73, 25, 19, 61, 19, 74, 74, 73, 73, 74, 25, 73, 69, 73, 3779940778, 766597780, 74, 128, 19, 73, 74, 26, 17, 129, 74, 64, 74, 74, 74, 71, 74, 73, 26, 74, 74, 74, 74, 74, 65, 71, 130, 73, 71, 74, 74, 4189603804, 62, 74, 1849344424, 73, 73, 74, 74, 74, 74, 74, 71, 74, 71, 73, 131, 115, 67, 3244628653, 72, 4266069071, 66, 73, 74, 72, 67, 73, 74, 74, 72, 74, 73, 73, 73, 70, 73, 74, 74, 74, 74, 74, 74, 72, 71, 74, 72, 74, 68, 73, 74, 74, 72, 73, 75, 3938, 73, 74, 73, 74, 67, 67, 141, 2399674015, 68, 79, 115, 67, 3491835384, 68, 1345404210, 79, 142, 132, 27, 520931705, 66, 73, 74, 28, 27, 73, 73, 73, 19, 73, 73, 73, 74, 61, 19, 73, 73, 74, 74, 74, 28, 73, 69, 73, 1634177178, 1927526375, 74, 133, 19, 73, 73, 29, 17, 134, 73, 64, 73, 73, 74, 71, 74, 73, 29, 74, 74, 74, 73, 74, 65, 71, 135, 73, 71, 74, 73, 4197545127, 62, 74, 3561389419, 73, 74, 74, 74, 73, 74, 74, 71, 74, 71, 74, 136, 115, 67, 128012797, 72, 2951122383, 66, 74, 74, 72, 67, 73, 73, 73, 72, 73, 73, 73, 73, 70, 73, 73, 74, 73, 74, 73, 73, 71, 72, 73, 72, 73, 68, 73, 73, 73, 72, 74, 75, 4068, 73, 73, 73, 73, 67, 67, 141, 4135521917, 68, 79, 115, 67, 2496093414, 68, 623273562, 79, 142, 137, 30, 304286489, 66, 73, 73, 31, 30, 73, 73, 73, 19, 74, 73, 73, 73, 61, 19, 74, 73, 73, 73, 74, 31, 73, 69, 74, 2188014201, 1690007430, 73, 138, 19, 73, 74, 32, 17, 139, 73, 64, 74, 73, 74, 71, 73, 74, 32, 74, 74, 73, 73, 74, 65, 71, 140, 74, 71, 73, 74, 3668389281, 62, 74, 3642278191, 74, 74, 74, 73, 73, 73, 74, 71, 74, 71, 73, 141, 115, 67, 86108748, 72, 2008383750, 66, 73, 73, 72, 67, 74, 73, 74, 72, 73, 74, 73, 74, 70, 74, 73, 73, 74, 74, 73, 74, 72, 71, 74, 72, 74, 68, 74, 73, 73, 72, 74, 75, 4198, 74, 74, 74, 74, 67, 67, 141, 2248864386, 68, 79, 115, 67, 2261998942, 68, 112449778, 79, 142, 142, 33, 2424823279, 66, 74, 74, 34, 33, 74, 74, 74, 19, 74, 74, 74, 74, 61, 19, 74, 74, 74, 73, 73, 34, 74, 69, 73, 2953620008, 4110969069, 74, 143, 19, 73, 74, 35, 17, 144, 73, 64, 73, 74, 73, 71, 73, 74, 35, 74, 74, 73, 73, 73, 65, 71, 145, 74, 71, 74, 74, 56909811, 62, 74, 2358684133, 74, 74, 74, 73, 73, 74, 74, 71, 74, 71, 74, 146, 115, 67, 3936388687, 72, 1804908061, 66, 73, 74, 72, 67, 73, 74, 74, 72, 73, 74, 73, 73, 70, 74, 73, 73, 73, 73, 74, 73, 72, 71, 74, 72, 73, 68, 74, 73, 74, 72, 74, 75, 4328, 73, 73, 73, 73, 67, 67, 141, 694805070, 68, 79, 115, 67, 570126466, 68, 773434374, 79, 142, 147, 36, 243644961, 68, 73, 73, 73, 36, 73, 75, 4356, 74, 74, 73, 74, 37, 19, 61, 19, 73, 73, 74, 73, 73, 37, 73, 69, 73, 3056979894, 1319572676, 74, 148, 19, 73, 74, 38, 17, 149, 73, 64, 73, 73, 74, 71, 74, 73, 38, 74, 73, 73, 74, 74, 65, 71, 150, 73, 71, 74, 74, 2145124453, 62, 74, 4261916096, 73, 74, 73, 73, 74, 74, 74, 71, 73, 71, 74, 151, 115, 67, 2794450431, 72, 421758137, 66, 74, 73, 72, 67, 73, 74, 74, 72, 74, 74, 73, 73, 70, 74, 73, 73, 73, 73, 73, 73, 72, 71, 73, 72, 73, 68, 73, 73, 73, 72, 74, 75, 4459, 74, 74, 73, 74, 67, 67, 141, 2782010823, 68, 79, 115, 67, 3992209104, 68, 1790772050, 79, 142, 152, 39, 3501929725, 68, 73, 74, 74, 39, 73, 75, 4487, 73, 74, 74, 74, 40, 19, 61, 19, 74, 73, 74, 73, 74, 40, 73, 69, 74, 2235741300, 1017614124, 74, 153, 19, 73, 73, 41, 17, 154, 73, 64, 73, 74, 74, 71, 74, 73, 41, 73, 73, 74, 74, 74, 65, 71, 155, 74, 71, 74, 73, 205378256, 115, 67, 4228245906, 72, 2724382152, 66, 73, 73, 72, 67, 73, 74, 73, 72, 74, 73, 74, 74, 67, 73, 72, 74, 74, 73, 74, 74, 73, 74, 71, 72, 68, 73, 74, 73, 72, 74, 75, 4574, 74, 73, 74, 74, 67, 67, 141, 2864546278, 68, 79, 115, 67, 1262175178, 68, 2460344363, 79, 142, 156, 42, 340513285, 66, 73, 74, 43, 42, 73, 73, 74, 19, 74, 74, 73, 74, 61, 19, 74, 74, 74, 74, 73, 43, 74, 69, 73, 3462814853, 1453366530, 74, 157, 19, 73, 73, 44, 17, 158, 74, 64, 74, 73, 73, 71, 73, 74, 44, 73, 73, 73, 74, 73, 65, 71, 159, 73, 71, 73, 74, 1996987675, 115, 67, 3455757657, 72, 1533482655, 66, 74, 74, 72, 67, 74, 73, 73, 72, 74, 73, 74, 74, 67, 74, 72, 73, 74, 74, 74, 74, 73, 73, 71, 72, 68, 74, 74, 73, 72, 74, 75, 4688, 74, 74, 73, 73, 67, 67, 141, 3626500068, 68, 79, 115, 67, 1638269753, 68, 1625633626, 79, 142, 160, 45, 1624744417, 68, 73, 74, 74, 45, 73, 75, 4716, 74, 73, 73, 74, 46, 19, 61, 19, 74, 73, 73, 74, 74, 46, 73, 69, 73, 2383671960, 2451416872, 73, 161, 19, 73, 73, 47, 17, 162, 74, 64, 74, 74, 73, 71, 73, 73, 47, 74, 74, 74, 73, 73, 65, 71, 163, 73, 71, 74, 74, 2936262915, 62, 73, 1800582803, 74, 73, 74, 74, 74, 74, 73, 71, 73, 71, 74, 164, 115, 67, 292190201, 72, 622001659, 66, 73, 73, 72, 67, 74, 74, 74, 72, 74, 74, 73, 73, 70, 74, 73, 73, 74, 74, 74, 74, 72, 71, 73, 72, 74, 68, 73, 74, 74, 72, 74, 75, 4819, 73, 74, 73, 73, 67, 67, 141, 1716579272, 68, 79, 115, 67, 3853797181, 68, 2600192910, 79, 142, 165, 48, 3524809801, 66, 74, 73, 49, 48, 73, 74, 73, 19, 74, 73, 73, 73, 61, 19, 73, 73, 74, 73, 73, 49, 73, 69, 73, 3440785163, 1009156648, 74, 166, 19, 73, 73, 50, 17, 167, 73, 64, 73, 73, 73, 71, 74, 73, 50, 74, 74, 73, 74, 73, 65, 71, 168, 73, 71, 74, 73, 4239588714, 62, 73, 786947237, 73, 73, 73, 74, 73, 74, 74, 71, 73, 71, 74, 169, 115, 67, 1424868642, 72, 3797560936, 66, 73, 74, 72, 67, 73, 73, 73, 72, 73, 73, 73, 73, 70, 73, 74, 74, 73, 74, 74, 74, 72, 71, 73, 72, 74, 68, 73, 74, 74, 72, 73, 75, 4949, 74, 73, 73, 74, 67, 67, 141, 4210074709, 68, 79, 115, 67, 730840919, 68, 622493331, 79, 142, 170, 51, 817999939, 68, 74, 74, 74, 51, 74, 75, 4977, 74, 74, 74, 74, 52, 19, 61, 19, 74, 74, 74, 73, 73, 52, 73, 69, 73, 270131608, 3427521628, 73, 171, 19, 73, 74, 53, 17, 172, 74, 64, 74, 73, 74, 71, 74, 73, 53, 74, 74, 73, 73, 74, 65, 71, 173, 74, 71, 74, 73, 3471664073, 115, 67, 1509467479, 72, 4091365397, 66, 74, 73, 72, 67, 73, 74, 73, 72, 74, 73, 74, 73, 67, 73, 72, 73, 73, 73, 74, 74, 73, 74, 71, 72, 68, 73, 73, 73, 72, 74, 75, 5064, 74, 73, 74, 74, 67, 67, 141, 1522440109, 68, 79, 115, 67, 2840824265, 68, 2464480081, 79, 142, 174, 54, 3187090949, 66, 74, 74, 55, 54, 73, 74, 73, 19, 74, 74, 74, 74, 61, 19, 73, 73, 74, 73, 73, 55, 73, 69, 73, 702396968, 3377204676, 74, 175, 19, 73, 73, 56, 17, 176, 74, 64, 73, 74, 73, 71, 74, 74, 56, 74, 74, 74, 74, 74, 65, 71, 177, 73, 71, 73, 74, 2472463079, 62, 74, 4045900876, 74, 73, 73, 74, 74, 74, 74, 71, 74, 71, 74, 178, 115, 67, 4020463823, 72, 1438565905, 66, 74, 73, 72, 67, 74, 73, 74, 72, 73, 74, 74, 73, 70, 73, 74, 73, 74, 73, 74, 74, 72, 71, 74, 72, 74, 68, 74, 74, 74, 72, 73, 75, 5194, 73, 73, 73, 73, 67, 67, 141, 2933697009, 68, 79, 115, 67, 4209875436, 68, 4119203144, 79, 142, 179, 57, 1242780453, 66, 73, 74, 58, 57, 73, 73, 73, 19, 73, 73, 73, 74, 61, 19, 74, 73, 74, 73, 73, 58, 73, 69, 74, 3239706118, 1693155791, 73, 180, 19, 74, 74, 59, 17, 181, 73, 64, 74, 74, 74, 71, 73, 74, 59, 73, 74, 73, 74, 73, 65, 71, 182, 74, 71, 73, 73, 3403368744, 115, 67, 2413667877, 72, 2743479335, 66, 73, 74, 72, 67, 74, 73, 74, 72, 73, 74, 74, 74, 67, 74, 72, 73, 74, 74, 73, 74, 74, 73, 71, 72, 68, 74, 73, 74, 72, 73, 75, 5308, 74, 74, 73, 74, 67, 67, 141, 1729535317, 68, 79, 115, 67, 19928639, 68, 3406866752, 79, 142, 183, 60, 3374268379, 68, 74, 74, 74, 60, 74, 75, 5336, 74, 74, 74, 74, 61, 19, 61, 19, 74, 74, 73, 73, 74, 61, 73, 69, 74, 1564766693, 2131953564, 74, 184, 19, 74, 74, 62, 17, 185, 73, 64, 74, 73, 73, 71, 74, 73, 62, 73, 73, 74, 73, 74, 65, 71, 186, 73, 71, 73, 73, 2635489585, 115, 67, 1608992479, 72, 1938803937, 66, 73, 73, 72, 67, 74, 74, 74, 72, 73, 74, 73, 74, 67, 74, 72, 73, 74, 73, 74, 74, 74, 73, 71, 72, 68, 74, 73, 74, 72, 74, 75, 5423, 73, 73, 73, 73, 67, 67, 141, 1875783622, 68, 79, 115, 67, 2487978119, 68, 1324091772, 79, 142, 187, 63, 1802449831, 66, 73, 73, 64, 63, 73, 74, 74, 19, 73, 74, 73, 73, 61, 19, 73, 73, 73, 73, 74, 64, 74, 69, 74, 3497095840, 177169907, 74, 188, 19, 73, 74, 65, 17, 189, 73, 64, 73, 74, 74, 71, 74, 73, 65, 73, 74, 73, 73, 74, 65, 71, 190, 73, 71, 74, 74, 54240388, 115, 67, 2093913813, 72, 622056103, 66, 73, 74, 72, 67, 73, 74, 74, 72, 74, 74, 73, 74, 67, 73, 72, 73, 73, 73, 74, 74, 73, 74, 72, 71, 68, 73, 73, 74, 72, 73, 75, 5537, 74, 73, 74, 73, 67, 67, 141, 3033986835, 68, 79, 115, 67, 2968707705, 68, 4237220591, 79, 115, 67, 19541378, 68, 976519898, 79, 142, 191, 77, 1827617348, 111, 118, 13, 5, 2806765098, 66, 30116, 68, 73, 73, 74, 66, 74, 75, 5578, 73, 74, 73, 73, 17, 17, 115, 67, 2142941078, 68, 921599928, 79, 119, 77, 192, 2929620535, 78, 3, 77, 71, 73, 74, 74, 74, 73, 2, 78, 74, 5, 77, 73, 74, 73, 64, 74, 74, 73, 71, 74, 73, 77, 74, 73, 74, 74, 74, 65, 71, 193, 73, 71, 74, 74, 2391515782, 115, 67, 1343239016, 72, 740209846, 66, 74, 73, 72, 67, 74, 73, 73, 72, 74, 73, 73, 74, 67, 74, 72, 74, 73, 74, 73, 73, 74, 74, 71, 72, 68, 74, 74, 74, 72, 74, 75, 5670, 73, 74, 73, 73, 67, 67, 141, 1703112699, 68, 79, 141, 24239, 66, 66, 73, 74, 17, 66, 74, 74, 74, 17, 74, 74, 73, 73, 115, 67, 2579066054, 68, 3612577560, 79, 141, 24387, 66, 66, 73, 74, 17, 66, 73, 74, 74, 17, 74, 74, 73, 74, 115, 67, 826912369, 68, 1681983480, 79, 142, 10, 77, 2003283263, 61, 5, 74, 73, 73, 73, 74, 77, 73, 141, 12456, 66, 66, 74, 74, 17, 66, 74, 73, 73, 17, 74, 74, 74, 74, 115, 67, 2670930836, 68, 2101701233, 79, 68, 74, 74, 74, 13, 74, 75, 5767, 73, 73, 73, 73, 77, 5, 43, 12, 3, 11, 1, 77, 118, 14, 194, 4245827709, 66, 10731, 66, 73, 74, 17, 66, 73, 73, 74, 17, 73, 73, 74, 74, 115, 67, 250597865, 68, 2951870819, 79, 142, 8, 77, 0, 112, 68, 73, 73, 74, 9, 73, 75, 5817, 74, 73, 73, 73, 77, 4, 61, 4, 74, 73, 73, 73, 74, 77, 73, 141, 24089, 66, 66, 74, 73, 17, 66, 73, 74, 73, 17, 73, 74, 73, 73, 115, 67, 188186640, 68, 1877656854, 79, 122, 7, 195, 312848892, 77, 196, 3838130352, 8, 7, 77, 141, 10381, 66, 68, 74, 74, 74, 66, 73, 75, 5875, 74, 74, 74, 74, 17, 17, 115, 67, 2373465357, 68, 679762602, 79, 119, 77, 191, 1827617348, 78, 3, 77, 43, 10, 3, 78, 1, 5, 141, 8243, 66, 66, 74, 74, 17, 66, 73, 74, 74, 17, 74, 73, 73, 73, 115, 67, 607700112, 68, 1133597568, 79, 141, 2905, 66, 71, 74, 73, 73, 74, 74, 2, 66, 74, 17, 66, 73, 74, 73, 64, 74, 73, 73, 71, 74, 73, 66, 74, 74, 73, 74, 74, 65, 71, 197, 74, 71, 74, 73, 22998799, 115, 67, 1265788803, 72, 2242929326, 66, 73, 74, 72, 67, 74, 73, 73, 72, 74, 74, 74, 73, 67, 74, 72, 73, 73, 73, 73, 74, 74, 74, 71, 72, 68, 73, 73, 73, 72, 74, 75, 5998, 73, 74, 74, 74, 67, 67, 141, 1313861408, 68, 79, 141, 6894, 66, 68, 74, 73, 73, 66, 74, 75, 6019, 74, 74, 74, 74, 17, 17, 115, 67, 1774146308, 68, 1320757306, 79, 141, 43650, 66, 66, 74, 74, 17, 66, 74, 74, 73, 17, 73, 73, 74, 74, 115, 67, 720684954, 68, 595000286, 79, 143, 4, 141, 63497, 66, 71, 73, 73, 73, 73, 74, 0, 66, 74, 17, 66, 74, 74, 74, 64, 73, 74, 74, 71, 74, 73, 66, 74, 74, 74, 74, 74, 65, 71, 198, 73, 71, 73, 74, 3578739689, 62, 74, 2631384148, 74, 73, 74, 74, 73, 74, 74, 71, 74, 71, 73, 199, 115, 67, 6316418, 72, 1027756836, 66, 74, 74, 72, 67, 74, 74, 74, 72, 74, 74, 74, 74, 70, 74, 73, 74, 74, 73, 74, 74, 71, 72, 74, 72, 73, 68, 74, 74, 74, 72, 74, 75, 6147, 74, 73, 74, 74, 67, 67, 141, 3801370869, 68, 79, 141, 1241, 66, 66, 74, 74, 17, 66, 74, 74, 73, 17, 74, 74, 74, 73, 115, 67, 2418763199, 68, 3651384648, 79, 141, 51785, 66, 66, 73, 73, 17, 66, 74, 74, 74, 17, 73, 74, 74, 74, 115, 67, 2381983445, 68, 1351575358, 79, 145, 2, 144, 3133572145, 200, 77, 42, 6, 77, 1, 0, 141, 60203, 66, 66, 74, 73, 17, 66, 73, 73, 73, 17, 74, 73, 73, 73, 115, 67, 2109784093, 68, 1682928028, 79, 61, 3, 73, 74, 74, 74, 74, 6, 74, 142, 201, 77, 4050745518, 61, 4, 73, 74, 74, 73, 73, 77, 73, 141, 50544, 66, 68, 74, 74, 74, 66, 74, 75, 6267, 74, 74, 74, 73, 17, 17, 115, 67, 2981655738, 68, 3130502002, 79, 142, 202, 77, 4101372167, 68, 74, 74, 73, 77, 74, 75, 6291, 73, 73, 73, 73, 78, 5, 61, 5, 74, 73, 74, 74, 74, 78, 73, 141, 18533, 66, 68, 74, 73, 74, 66, 73, 75, 6317, 74, 74, 74, 73, 17, 17, 115, 67, 3235998676, 68, 59211, 79, 72, 74, 73, 73, 74, 12, 73, 74, 74, 14, 74, 77, 73, 73, 73, 78, 74, 10, 74, 74, 73, 74, 77, 74, 43, 9, 7, 8, 1, 78, 141, 8420, 66, 68, 74, 73, 74, 66, 74, 75, 6370, 73, 73, 74, 73, 17, 17, 115, 67, 2376318877, 68, 446559969, 79, 42, 67, 69, 2, 68, 67, 119, 70, 203, 2170707436, 67, 67, 70, 113, 120, 89, 204, 3504205126, 90, 205, 1468504095, 91, 206, 3566775539, 93, 10, 2003283263, 142, 207, 92, 3158063774, 47, 86, 10292, 2, 13, 0, 0, 125, 20, 21, 208, 3038054432, 61, 22, 89, 89, 90, 91, 91, 21, 91, 142, 10, 23, 2003283263, 61, 24, 91, 91, 91, 91, 90, 23, 89, 142, 209, 25, 2514872181, 63, 89, 90, 89, 22, 6458, 89, 89, 25, 93, 89, 26, 90, 64, 89, 91, 90, 87, 90, 89, 26, 91, 90, 89, 91, 91, 65, 87, 210, 89, 87, 91, 89, 2438622002, 62, 91, 3777828159, 89, 91, 90, 90, 91, 90, 90, 87, 91, 87, 91, 211, 115, 84, 2714977545, 88, 441974758, 66, 91, 90, 88, 84, 89, 91, 90, 88, 90, 89, 90, 91, 70, 91, 90, 90, 89, 91, 91, 90, 88, 87, 89, 88, 89, 68, 90, 91, 90, 88, 89, 92, 6539, 91, 91, 91, 90, 84, 84, 141, 3087065891, 85, 78, 142, 212, 27, 2091900777, 61, 24, 91, 89, 89, 89, 91, 27, 91, 69, 90, 3012990211, 3425192300, 90, 213, 24, 90, 91, 28, 22, 214, 91, 64, 89, 90, 91, 87, 91, 91, 28, 89, 91, 91, 89, 90, 65, 87, 215, 89, 87, 91, 90, 2449974536, 115, 84, 987020385, 88, 4019399777, 66, 89, 91, 88, 84, 90, 90, 89, 88, 90, 91, 91, 90, 67, 89, 88, 89, 91, 89, 90, 90, 91, 90, 87, 88, 68, 90, 89, 90, 88, 89, 92, 6634, 90, 90, 89, 89, 84, 84, 141, 900847287, 85, 78, 115, 84, 2519736016, 85, 2787577191, 78, 142, 216, 29, 883116005, 68, 91, 89, 89, 29, 91, 92, 6662, 90, 90, 91, 90, 30, 24, 61, 24, 89, 91, 91, 90, 91, 30, 91, 69, 89, 469467787, 2345208472, 91, 217, 24, 90, 89, 31, 22, 218, 90, 64, 90, 89, 90, 87, 90, 90, 31, 89, 90, 89, 90, 89, 65, 87, 219, 90, 87, 89, 89, 1348225304, 62, 91, 2159004886, 91, 89, 90, 91, 90, 90, 89, 87, 90, 87, 89, 220, 115, 84, 421557500, 88, 1687799036, 66, 89, 91, 88, 84, 89, 89, 90, 88, 89, 91, 91, 91, 70, 91, 89, 91, 89, 91, 90, 89, 87, 88, 90, 88, 89, 68, 89, 90, 90, 88, 91, 92, 6765, 89, 91, 90, 89, 84, 84, 141, 2733345834, 85, 78, 115, 84, 3770387023, 85, 2869005990, 78, 142, 221, 32, 2785696150, 66, 91, 90, 33, 32, 89, 89, 89, 24, 90, 89, 91, 91, 61, 24, 89, 89, 89, 89, 89, 33, 89, 69, 91, 2347950498, 3669046614, 91, 222, 24, 91, 90, 34, 22, 223, 90, 64, 91, 91, 91, 87, 90, 89, 34, 90, 91, 90, 91, 90, 65, 87, 224, 89, 87, 91, 90, 1979750997, 62, 91, 2922119824, 91, 90, 91, 89, 89, 90, 91, 87, 91, 87, 89, 225, 115, 84, 2545014924, 88, 3806931084, 66, 90, 90, 88, 84, 89, 89, 89, 88, 90, 89, 89, 91, 70, 89, 90, 91, 91, 89, 89, 91, 87, 88, 91, 88, 89, 68, 91, 91, 90, 88, 91, 92, 6895, 89, 89, 91, 91, 84, 84, 141, 3339643299, 85, 78, 115, 84, 1929730651, 85, 317253778, 78, 142, 226, 35, 2677010570, 66, 91, 90, 36, 35, 91, 90, 90, 24, 91, 90, 91, 91, 61, 24, 90, 90, 89, 91, 89, 36, 91, 69, 91, 1660894047, 1618561862, 90, 227, 24, 91, 90, 37, 22, 228, 90, 64, 90, 90, 90, 87, 90, 91, 37, 90, 89, 91, 90, 90, 65, 87, 229, 91, 87, 91, 90, 912780768, 115, 84, 1284775167, 88, 2546822399, 66, 91, 90, 88, 84, 91, 91, 91, 88, 90, 91, 90, 89, 67, 90, 88, 91, 89, 91, 91, 91, 91, 89, 88, 87, 68, 89, 89, 90, 88, 91, 92, 7009, 90, 91, 90, 91, 84, 84, 141, 2799014052, 85, 78, 115, 84, 4191659862, 85, 599744976, 78, 142, 230, 38, 2910559474, 68, 89, 90, 90, 38, 90, 92, 7037, 90, 89, 91, 89, 39, 24, 61, 24, 89, 91, 89, 89, 91, 39, 90, 69, 89, 776803522, 3128690364, 90, 231, 24, 91, 91, 40, 22, 232, 89, 64, 90, 89, 89, 87, 91, 89, 40, 89, 91, 89, 89, 89, 65, 87, 233, 91, 87, 89, 91, 2957097917, 62, 90, 532278891, 91, 90, 90, 91, 89, 89, 91, 87, 89, 87, 89, 234, 115, 84, 158312669, 88, 3191232733, 66, 90, 89, 88, 84, 91, 90, 89, 88, 91, 89, 90, 90, 70, 91, 90, 91, 91, 90, 89, 91, 87, 88, 89, 88, 89, 68, 90, 90, 89, 88, 89, 92, 7140, 91, 91, 90, 89, 84, 84, 141, 2034828322, 85, 78, 115, 84, 3021318608, 85, 4141437238, 78, 142, 235, 41, 725710644, 68, 89, 89, 91, 41, 91, 92, 7168, 89, 89, 91, 89, 42, 24, 61, 24, 90, 91, 89, 90, 90, 42, 89, 69, 89, 844360001, 1020421873, 91, 236, 24, 89, 90, 43, 22, 237, 90, 64, 90, 89, 89, 87, 89, 89, 43, 89, 91, 90, 89, 89, 65, 87, 238, 90, 87, 89, 90, 1270597171, 115, 84, 1311577490, 88, 3417703826, 66, 89, 89, 88, 84, 89, 90, 90, 88, 91, 89, 89, 89, 67, 89, 88, 89, 89, 89, 91, 89, 90, 90, 87, 88, 68, 90, 89, 89, 88, 91, 92, 7255, 90, 89, 91, 91, 84, 84, 141, 2989679758, 85, 78, 115, 84, 970207288, 85, 4064892994, 78, 142, 239, 44, 3451312555, 66, 90, 89, 45, 44, 89, 89, 89, 24, 90, 89, 90, 90, 61, 24, 90, 90, 91, 91, 91, 45, 89, 69, 89, 2760993231, 66793410, 89, 240, 24, 89, 91, 46, 22, 241, 91, 64, 90, 89, 89, 87, 91, 91, 46, 90, 90, 90, 89, 91, 65, 87, 242, 90, 87, 90, 89, 3458791790, 115, 84, 1645263188, 88, 2790926676, 66, 90, 89, 88, 84, 91, 90, 90, 88, 90, 90, 91, 89, 67, 90, 88, 90, 90, 90, 90, 90, 91, 89, 87, 88, 68, 89, 90, 90, 88, 89, 92, 7369, 90, 90, 91, 89, 84, 84, 141, 1535374135, 85, 78, 115, 84, 712508473, 85, 296254510, 78, 142, 243, 47, 2255738029, 66, 89, 89, 48, 47, 89, 89, 90, 24, 90, 90, 90, 89, 61, 24, 89, 91, 91, 90, 89, 48, 89, 69, 89, 2252532368, 3377297808, 89, 244, 24, 91, 89, 49, 22, 245, 89, 64, 91, 90, 91, 87, 89, 89, 49, 89, 90, 89, 90, 91, 65, 87, 246, 90, 87, 90, 89, 1037210773, 115, 84, 4212135926, 88, 1644828273, 66, 89, 90, 88, 84, 90, 91, 90, 88, 90, 90, 90, 90, 67, 89, 88, 90, 91, 91, 89, 91, 90, 91, 87, 88, 68, 90, 91, 91, 88, 90, 92, 7483, 90, 91, 90, 91, 84, 84, 141, 2731859801, 85, 78, 115, 84, 589388185, 85, 2659674800, 78, 142, 247, 50, 167815100, 68, 89, 90, 89, 50, 89, 92, 7511, 89, 90, 89, 90, 51, 24, 61, 24, 90, 89, 89, 91, 89, 51, 90, 69, 90, 2384401682, 3889533021, 90, 248, 24, 90, 91, 52, 22, 249, 90, 64, 91, 91, 89, 87, 89, 91, 52, 90, 90, 91, 91, 89, 65, 87, 250, 91, 87, 91, 89, 1046044012, 115, 84, 3297774503, 88, 2035858343, 66, 91, 90, 88, 84, 89, 89, 91, 88, 90, 90, 90, 89, 67, 91, 88, 90, 91, 89, 89, 91, 90, 90, 87, 88, 68, 89, 89, 89, 88, 90, 92, 7598, 89, 90, 89, 89, 84, 84, 141, 1423838471, 85, 78, 115, 84, 4176979373, 85, 1509320556, 78, 142, 251, 53, 2186886797, 68, 89, 91, 90, 53, 91, 92, 7626, 91, 91, 90, 91, 54, 24, 61, 24, 91, 91, 91, 90, 89, 54, 89, 69, 91, 3308509207, 259073775, 89, 252, 24, 91, 90, 55, 22, 253, 91, 64, 89, 89, 90, 87, 91, 91, 55, 90, 91, 89, 89, 89, 65, 87, 254, 90, 87, 89, 89, 3066430172, 115, 84, 3318113117, 88, 3623383901, 66, 90, 90, 88, 84, 89, 90, 89, 88, 90, 90, 91, 90, 67, 90, 88, 90, 89, 89, 89, 89, 90, 91, 87, 88, 68, 91, 89, 91, 88, 90, 92, 7713, 90, 90, 91, 90, 84, 84, 141, 1709519964, 85, 78, 115, 84, 3897427139, 85, 4183662468, 78, 142, 255, 56, 4173267633, 66, 89, 89, 57, 56, 91, 91, 91, 24, 89, 91, 89, 89, 61, 24, 89, 89, 89, 89, 89, 57, 91, 69, 91, 3397999570, 1209367023, 91, 256, 24, 90, 91, 58, 22, 257, 89, 64, 90, 89, 89, 87, 91, 90, 58, 90, 89, 89, 89, 89, 65, 87, 258, 91, 87, 91, 89, 1228366972, 62, 90, 3846021848, 90, 89, 91, 89, 89, 91, 91, 87, 91, 87, 91, 259, 115, 84, 3447287061, 88, 3865140501, 66, 91, 90, 88, 84, 91, 89, 90, 88, 89, 91, 91, 91, 70, 89, 91, 89, 91, 91, 90, 91, 88, 87, 91, 88, 89, 68, 91, 90, 89, 88, 91, 92, 7843, 91, 90, 91, 91, 84, 84, 141, 3733686898, 85, 78, 115, 84, 364669178, 85, 3005374172, 78, 142, 260, 59, 3795321975, 68, 90, 89, 89, 59, 91, 92, 7871, 89, 89, 91, 90, 60, 24, 61, 24, 89, 91, 91, 91, 89, 60, 89, 69, 89, 2948995229, 1355226448, 91, 261, 24, 89, 91, 61, 22, 262, 91, 64, 91, 91, 91, 87, 91, 91, 61, 90, 90, 89, 89, 91, 65, 87, 263, 90, 87, 90, 90, 1143289735, 115, 84, 3175066931, 88, 142032179, 66, 90, 90, 88, 84, 91, 90, 90, 88, 90, 90, 89, 91, 67, 90, 88, 90, 90, 89, 91, 91, 91, 89, 88, 87, 68, 91, 89, 90, 88, 89, 92, 7958, 89, 89, 90, 89, 84, 84, 141, 2148725595, 85, 78, 115, 84, 683073018, 85, 3240106435, 78, 142, 264, 62, 289228620, 68, 90, 91, 91, 62, 91, 92, 7986, 91, 91, 91, 91, 63, 24, 61, 24, 89, 89, 91, 91, 91, 63, 89, 69, 90, 1258421625, 3460684709, 91, 265, 24, 89, 90, 64, 22, 266, 90, 64, 91, 89, 90, 87, 90, 91, 64, 90, 91, 91, 89, 90, 65, 87, 267, 91, 87, 90, 90, 2272798420, 115, 84, 2348472588, 88, 3576834316, 66, 90, 89, 88, 84, 90, 91, 90, 88, 89, 89, 89, 90, 67, 89, 88, 90, 90, 89, 90, 89, 90, 89, 87, 88, 68, 90, 90, 90, 88, 91, 92, 8073, 90, 90, 89, 90, 84, 84, 141, 685027604, 85, 78, 115, 84, 1492767051, 85, 1293273844, 78, 142, 268, 65, 4151556674, 66, 90, 89, 66, 65, 91, 90, 89, 24, 89, 89, 89, 90, 61, 24, 90, 91, 91, 91, 90, 66, 91, 69, 90, 3180906272, 2576997522, 90, 269, 24, 90, 90, 67, 22, 270, 90, 64, 90, 90, 90, 87, 90, 91, 67, 91, 91, 90, 90, 91, 65, 87, 271, 90, 87, 91, 90, 258210875, 62, 90, 912320700, 89, 89, 89, 90, 90, 90, 90, 87, 89, 87, 89, 272, 115, 84, 2764489717, 88, 2346521589, 66, 90, 89, 88, 84, 91, 90, 91, 88, 91, 90, 91, 89, 70, 90, 91, 90, 89, 90, 90, 91, 88, 87, 90, 88, 91, 68, 89, 90, 90, 88, 90, 92, 8203, 91, 91, 91, 90, 84, 84, 141, 3960859652, 85, 78, 115, 84, 3917263421, 85, 2799637080, 78, 142, 273, 68, 2367880384, 66, 90, 90, 69, 68, 90, 90, 90, 24, 90, 90, 91, 91, 61, 24, 90, 91, 91, 90, 91, 69, 90, 69, 91, 3609424559, 16860281, 90, 274, 24, 91, 91, 70, 22, 275, 91, 64, 90, 90, 89, 87, 90, 91, 70, 90, 90, 90, 91, 90, 65, 87, 276, 91, 87, 91, 90, 2081110685, 115, 84, 1330046129, 88, 3436057777, 66, 89, 90, 88, 84, 90, 90, 90, 88, 90, 90, 90, 90, 67, 91, 88, 90, 90, 91, 91, 90, 90, 91, 88, 87, 68, 90, 91, 91, 88, 90, 92, 8317, 90, 90, 90, 90, 84, 84, 141, 2999127009, 85, 78, 115, 84, 3477197933, 85, 916012431, 78, 142, 277, 71, 2757321594, 66, 89, 89, 72, 71, 91, 89, 90, 24, 90, 90, 91, 89, 61, 24, 91, 90, 90, 91, 90, 72, 91, 69, 91, 1849573771, 2775294844, 89, 278, 24, 90, 89, 73, 22, 279, 89, 64, 91, 91, 90, 87, 90, 90, 73, 89, 90, 89, 91, 91, 65, 87, 280, 91, 87, 90, 90, 3763199858, 115, 84, 2063866198, 88, 4169992534, 66, 90, 89, 88, 84, 89, 89, 91, 88, 90, 90, 89, 91, 67, 90, 88, 90, 90, 91, 91, 91, 91, 90, 87, 88, 68, 89, 89, 91, 88, 90, 92, 8431, 90, 91, 91, 90, 84, 84, 141, 975053858, 85, 78, 115, 84, 3515196431, 85, 761356603, 78, 142, 281, 74, 1942551578, 66, 91, 91, 75, 74, 91, 91, 89, 24, 91, 89, 91, 91, 61, 24, 90, 89, 91, 91, 90, 75, 91, 69, 90, 1919471972, 2306226109, 90, 282, 24, 91, 90, 76, 22, 283, 91, 64, 91, 89, 91, 87, 90, 89, 76, 89, 91, 89, 90, 90, 65, 87, 284, 91, 87, 89, 90, 4028624337, 62, 89, 1424751716, 89, 89, 91, 91, 90, 90, 90, 87, 90, 87, 89, 285, 115, 84, 26919266, 88, 2128867682, 66, 90, 89, 88, 84, 90, 89, 89, 88, 90, 91, 89, 91, 70, 91, 90, 90, 89, 90, 91, 90, 88, 87, 91, 88, 91, 68, 91, 90, 89, 88, 91, 92, 8561, 89, 91, 90, 90, 84, 84, 141, 2568188692, 85, 78, 115, 84, 2394266513, 85, 3634642047, 78, 142, 286, 77, 4106901550, 68, 89, 91, 91, 77, 89, 92, 8589, 89, 89, 89, 89, 78, 24, 61, 24, 89, 90, 91, 91, 91, 78, 89, 69, 91, 1705169292, 957752489, 91, 287, 24, 89, 91, 79, 22, 288, 90, 64, 91, 89, 91, 87, 91, 91, 79, 91, 91, 90, 89, 89, 65, 87, 289, 90, 87, 89, 89, 996262246, 115, 84, 1844361870, 88, 1542761102, 66, 90, 90, 88, 84, 89, 89, 91, 88, 90, 89, 89, 91, 67, 89, 88, 89, 89, 89, 89, 91, 89, 89, 87, 88, 68, 90, 91, 89, 88, 91, 92, 8676, 89, 89, 90, 90, 84, 84, 141, 2950635265, 85, 78, 115, 84, 3014571982, 85, 331080445, 78, 142, 290, 80, 1743024139, 66, 91, 90, 81, 80, 90, 90, 90, 24, 90, 90, 90, 90, 61, 24, 90, 90, 89, 89, 89, 81, 90, 69, 91, 807026315, 1616033925, 91, 291, 24, 91, 91, 82, 22, 292, 91, 64, 91, 91, 89, 87, 91, 89, 82, 91, 90, 91, 91, 89, 65, 87, 293, 91, 87, 89, 91, 2146557042, 62, 91, 377964106, 89, 90, 89, 91, 89, 90, 89, 87, 91, 87, 89, 294, 115, 84, 869662524, 88, 447483708, 66, 89, 90, 88, 84, 89, 89, 91, 88, 91, 89, 91, 91, 70, 90, 91, 89, 91, 90, 89, 90, 87, 88, 90, 88, 89, 68, 91, 89, 89, 88, 91, 92, 8806, 89, 89, 89, 89, 84, 84, 141, 698004603, 85, 78, 115, 84, 2174684485, 85, 2116120529, 78, 115, 84, 2184894280, 85, 2645865764, 78, 73, 94, 89, 9, 89, 91, 90, 90, 10, 90, 61, 1, 90, 90, 90, 90, 90, 94, 90, 118, 11, 295, 816589688, 83, 2095, 66, 89, 90, 22, 83, 91, 91, 89, 22, 91, 91, 91, 89, 115, 84, 4186708869, 85, 3285045077, 78, 125, 5, 94, 201, 4050745518, 61, 6, 91, 91, 91, 91, 90, 94, 91, 141, 9931, 83, 68, 91, 91, 89, 83, 89, 92, 8897, 90, 90, 91, 90, 22, 22, 115, 84, 1743647651, 85, 1767793159, 78, 141, 58441, 83, 66, 90, 90, 22, 83, 91, 90, 90, 22, 89, 90, 90, 90, 115, 84, 810460014, 85, 2993894963, 78, 70, 91, 89, 91, 89, 89, 91, 89, 12, 13, 90, 94, 89, 61, 8, 91, 90, 90, 90, 90, 94, 89, 144, 312848892, 195, 14, 141, 9292, 83, 68, 89, 90, 89, 83, 90, 92, 8968, 91, 90, 90, 90, 22, 22, 115, 84, 1080483045, 85, 3970371090, 78, 141, 22759, 83, 71, 89, 89, 89, 91, 89, 1, 83, 89, 22, 83, 89, 89, 89, 64, 89, 90, 89, 87, 91, 89, 83, 91, 89, 89, 90, 89, 65, 87, 296, 91, 87, 89, 89, 3695788253, 115, 84, 1814001293, 88, 3223984146, 66, 89, 89, 88, 84, 90, 91, 91, 88, 91, 91, 91, 91, 67, 89, 88, 91, 89, 89, 91, 91, 91, 89, 87, 88, 68, 89, 89, 90, 88, 89, 92, 9056, 89, 91, 91, 89, 84, 84, 141, 2451132207, 85, 78, 141, 0, 83, 68, 91, 90, 89, 83, 91, 92, 9077, 91, 90, 91, 91, 22, 22, 115, 84, 3002073777, 85, 204008148, 78, 141, 40742, 83, 68, 91, 91, 91, 83, 91, 92, 9100, 91, 91, 90, 91, 22, 22, 115, 84, 1763359459, 85, 1934967934, 78, 146, 94, 16, 0, 68, 90, 90, 91, 7, 90, 92, 9124, 91, 90, 91, 90, 95, 2, 43, 17, 0, 94, 1, 95, 141, 33409, 83, 66, 90, 91, 22, 83, 91, 91, 90, 22, 89, 91, 91, 89, 115, 84, 4057738503, 85, 1051458969, 78, 74, 90, 89, 8, 92, 91, 89, 9166, 94, 89, 90, 90, 17, 90, 43, 95, 14, 15, 1, 94, 68, 91, 90, 90, 95, 91, 92, 9186, 90, 90, 90, 90, 18, 6, 141, 7683, 83, 68, 89, 91, 90, 83, 90, 92, 9203, 89, 91, 90, 91, 22, 22, 115, 84, 2437377083, 85, 4238478275, 78, 123, 94, 196, 3838130352, 15, 14, 94, 16, 191, 1827617348, 141, 17520, 83, 66, 91, 91, 22, 83, 91, 91, 91, 22, 91, 91, 91, 89, 115, 84, 1099290773, 85, 1135414020, 78, 75, 89, 89, 89, 91, 91, 91, 1, 91, 11, 91, 94, 91, 89, 91, 74, 90, 91, 94, 92, 89, 91, 9270, 12, 89, 89, 89, 1, 90, 118, 13, 297, 3232489863, 83, 4139, 68, 89, 90, 90, 83, 89, 92, 9290, 89, 91, 90, 91, 22, 22, 115, 84, 2118247916, 85, 3621665090, 78, 141, 40855, 83, 71, 90, 90, 90, 90, 90, 2, 83, 91, 22, 83, 89, 90, 89, 64, 91, 90, 90, 87, 91, 90, 83, 91, 91, 89, 91, 90, 65, 87, 298, 91, 87, 91, 91, 3201459938, 115, 84, 303406708, 88, 457908717, 66, 91, 89, 88, 84, 91, 91, 90, 88, 89, 89, 91, 91, 67, 90, 88, 91, 91, 90, 91, 91, 91, 91, 87, 88, 68, 89, 90, 91, 88, 91, 92, 9378, 91, 91, 91, 89, 84, 84, 141, 2249099913, 85, 78, 141, 2135, 83, 66, 91, 90, 22, 83, 89, 89, 90, 22, 91, 90, 90, 89, 115, 84, 2897554010, 85, 2024156636, 78, 141, 10355, 83, 68, 91, 91, 90, 83, 90, 92, 9421, 89, 91, 91, 91, 22, 22, 115, 84, 3358680647, 85, 2321767936, 78, 141, 24910, 83, 66, 91, 90, 22, 83, 89, 90, 90, 22, 91, 90, 91, 91, 115, 84, 562180667, 85, 2714639935, 78, 61, 7, 91, 89, 91, 91, 89, 19, 89, 141, 21174, 83, 68, 90, 89, 89, 83, 89, 92, 9475, 89, 91, 89, 89, 22, 22, 115, 84, 2478939046, 85, 3475775234, 78, 71, 90, 89, 91, 90, 90, 2, 3, 90, 7, 94, 89, 90, 89, 64, 89, 90, 91, 87, 91, 90, 94, 89, 90, 91, 90, 90, 65, 87, 299, 90, 87, 90, 91, 3780598675, 62, 91, 3370162992, 90, 91, 90, 90, 90, 90, 91, 87, 90, 87, 90, 300, 115, 84, 2530010813, 88, 1042267837, 66, 90, 89, 88, 84, 90, 90, 90, 88, 89, 90, 91, 89, 70, 90, 90, 90, 89, 90, 91, 89, 87, 88, 90, 88, 91, 68, 89, 91, 91, 88, 90, 92, 9576, 90, 90, 91, 89, 84, 84, 141, 1641141975, 85, 78, 141, 12429, 83, 68, 90, 90, 89, 83, 90, 92, 9597, 90, 91, 89, 90, 22, 22, 115, 84, 711371991, 85, 3196874505, 78, 141, 2623, 83, 66, 89, 91, 22, 83, 89, 89, 89, 22, 90, 89, 89, 89, 115, 84, 3259358281, 85, 646275936, 78, 142, 301, 94, 932921037, 68, 90, 90, 90, 94, 90, 92, 9643, 90, 90, 90, 90, 9, 1, 118, 10, 10, 2003283263, 83, 25498, 68, 90, 90, 90, 83, 89, 92, 9663, 90, 90, 89, 90, 22, 22, 115, 84, 3667716107, 85, 3779219603, 78, 61, 6, 91, 90, 90, 90, 91, 18, 90, 141, 16795, 83, 68, 89, 89, 90, 83, 89, 92, 9695, 89, 89, 90, 89, 22, 22, 115, 84, 1792763360, 85, 1757263463, 78, 142, 10, 94, 2003283263, 61, 7, 89, 91, 91, 91, 89, 94, 91, 141, 11064, 83, 68, 89, 89, 91, 83, 89, 92, 9731, 90, 90, 91, 90, 22, 22, 115, 84, 595582278, 85, 3242846788, 78, 61, 94, 89, 91, 90, 89, 90, 7, 89, 142, 5, 94, 2806765098, 68, 89, 91, 90, 94, 90, 92, 9764, 89, 91, 90, 91, 19, 7, 141, 28934, 83, 66, 90, 91, 22, 83, 91, 90, 89, 22, 90, 89, 91, 91, 115, 84, 4288672820, 85, 1439971552, 78, 143, 6, 127, 94, 8, 0, 94, 42, 84, 86, 2, 85, 84, 114, 128, 7, 10, 2003283263, 5, 302, 284883807, 4, 303, 22891425, 6, 304, 3899115026, 3, 74, 4, 5, 1, 6, 4, 5, 9828, 8, 4, 4, 4, 0, 4, 142, 10, 9, 2003283263, 75, 4, 4, 5, 4, 4, 4, 8, 4, 9, 5, 10, 4, 5, 5, 61, 0, 4, 4, 5, 5, 5, 10, 4, 129, 8, 305, 2473189654, 9, 306, 1529234001, 10, 8, 9, 11, 307, 1011961573, 12, 8, 10, 2, 0, 11, 142, 10, 8, 2003283263, 75, 4, 5, 4, 4, 4, 4, 12, 5, 8, 4, 9, 5, 4, 4, 61, 0, 5, 5, 4, 5, 4, 9, 5, 142, 308, 8, 1768079384, 75, 4, 4, 4, 4, 4, 4, 0, 4, 8, 4, 9, 4, 5, 4, 74, 5, 4, 9, 6, 4, 4, 9936, 8, 4, 4, 5, 0, 4, 142, 10, 9, 2003283263, 75, 4, 5, 4, 5, 4, 5, 8, 4, 9, 4, 10, 4, 4, 5, 61, 0, 5, 4, 5, 5, 4, 10, 4, 142, 309, 8, 1433858609, 74, 4, 4, 8, 6, 5, 4, 9982, 9, 4, 5, 4, 0, 4, 142, 10, 8, 2003283263, 75, 5, 5, 5, 5, 5, 4, 9, 4, 8, 5, 10, 5, 5, 5, 61, 0, 5, 4, 5, 5, 5, 10, 4, 142, 310, 8, 4113101794, 68, 5, 4, 4, 8, 5, 6, 10028, 5, 5, 5, 4, 9, 0, 142, 10, 8, 2003283263, 75, 4, 4, 5, 4, 5, 5, 9, 5, 8, 4, 10, 5, 4, 4, 61, 0, 4, 4, 5, 4, 4, 10, 4, 48, 8, 1, 0, 143, 8, 127, 8, 8, 0, 8, 128, 6, 311, 3143690948, 5, 312, 3986101013, 4, 313, 422113578, 7, 10, 2003283263, 3, 142, 314, 8, 2891348676, 66, 4, 4, 9, 8, 4, 4, 4, 1, 4, 4, 4, 5, 142, 10, 8, 2003283263, 75, 5, 5, 4, 4, 4, 5, 9, 4, 8, 4, 10, 5, 5, 4, 61, 1, 4, 4, 4, 4, 5, 10, 5, 129, 8, 305, 2473189654, 9, 306, 1529234001, 10, 8, 9, 11, 315, 1153246797, 12, 8, 10, 2, 1, 11, 142, 10, 8, 2003283263, 75, 4, 4, 4, 5, 4, 4, 12, 5, 8, 5, 9, 5, 5, 5, 61, 1, 4, 4, 5, 4, 4, 9, 5, 142, 316, 8, 3056499453, 75, 5, 5, 4, 4, 5, 4, 1, 4, 8, 4, 9, 5, 4, 5, 74, 5, 4, 9, 6, 5, 4, 10206, 8, 5, 4, 5, 1, 4, 142, 10, 9, 2003283263, 75, 4, 4, 5, 4, 5, 5, 8, 5, 9, 5, 10, 5, 5, 4, 61, 1, 4, 4, 5, 5, 5, 10, 5, 68, 5, 5, 5, 0, 4, 6, 10248, 5, 5, 5, 5, 8, 1, 142, 10, 9, 2003283263, 75, 5, 4, 5, 5, 5, 4, 8, 5, 9, 5, 10, 5, 4, 5, 61, 1, 5, 4, 4, 4, 4, 10, 5, 142, 203, 8, 2170707436, 49, 9, 1, 8, 1, 143, 9, 127, 8, 8, 0, 8, 128, 4, 317, 1954656214, 7, 10, 2003283263, 6, 318, 1467698792, 5, 319, 2425727596, 3, 142, 320, 8, 907277803, 74, 5, 5, 8, 6, 4, 4, 10324, 9, 4, 4, 4, 1, 5, 142, 10, 8, 2003283263, 75, 4, 5, 4, 5, 5, 4, 9, 5, 8, 4, 10, 4, 4, 4, 61, 1, 4, 4, 4, 4, 5, 10, 4, 129, 8, 305, 2473189654, 9, 306, 1529234001, 10, 8, 9, 11, 321, 179634877, 12, 8, 10, 2, 1, 11, 142, 10, 8, 2003283263, 75, 5, 4, 4, 5, 5, 5, 12, 5, 8, 5, 9, 4, 5, 5, 61, 1, 4, 4, 4, 5, 5, 9, 4, 142, 322, 8, 946876243, 75, 4, 5, 4, 5, 5, 4, 1, 5, 8, 5, 9, 5, 4, 5, 74, 4, 4, 9, 6, 5, 5, 10432, 8, 5, 4, 4, 1, 5, 142, 10, 9, 2003283263, 75, 4, 4, 4, 4, 5, 4, 8, 5, 9, 5, 10, 5, 4, 5, 61, 1, 5, 5, 5, 4, 4, 10, 4, 76, 4, 4, 0, 5, 5, 4, 5, 1, 4, 8, 5, 5, 4, 4, 4, 142, 10, 9, 2003283263, 75, 5, 4, 4, 4, 5, 5, 8, 5, 9, 4, 10, 5, 5, 4, 61, 1, 4, 5, 4, 4, 4, 10, 4, 68, 5, 4, 5, 0, 5, 6, 10518, 4, 5, 5, 4, 8, 1, 142, 10, 9, 2003283263, 75, 5, 4, 4, 5, 5, 4, 8, 4, 9, 5, 10, 5, 4, 4, 61, 1, 5, 5, 5, 4, 4, 10, 4, 142, 323, 8, 3338525522, 72, 4, 5, 4, 4, 1, 4, 4, 4, 8, 4, 9, 5, 4, 142, 324, 8, 1497101516, 75, 5, 4, 4, 5, 4, 5, 1, 4, 8, 5, 10, 5, 5, 5, 73, 8, 4, 9, 5, 5, 5, 5, 10, 4, 142, 10, 9, 2003283263, 75, 5, 5, 4, 4, 5, 5, 8, 5, 9, 4, 10, 4, 5, 4, 61, 1, 5, 5, 5, 4, 5, 10, 5, 76, 4, 5, 1, 5, 5, 5, 5, 1, 5, 8, 5, 5, 4, 4, 5, 142, 10, 9, 2003283263, 75, 4, 4, 4, 5, 5, 5, 8, 5, 9, 5, 10, 4, 4, 4, 61, 1, 5, 4, 4, 5, 4, 10, 5, 143, 1, 127, 8, 8, 0, 8]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 20; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 77: + { + // JUMP_3556 (specialized) + regs[fp + SLOTS.PC] = 3556; + break; + } + case 78: + { + // JUMP_9793 (specialized) + regs[fp + SLOTS.PC] = 9793; + break; + } + case 79: + { + // JUMP_6376 (specialized) + regs[fp + SLOTS.PC] = 6376; + break; + } + case 80: + { + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 81: + { + // LOAD_INT_18_8 (specialized) + var dst = 18; + regs[base + dst] = 8; + break; + } + case 82: + { + // LOAD_CONST_14_0_1817320238 (specialized) + var dst = 14; + regs[base + dst] = this._constant(0, 1817320238); + break; + } + case 83: + { + // LOAD_CONST_13_1_2935018193 (specialized) + var dst = 13; + regs[base + dst] = this._constant(1, 2935018193); + break; + } + case 84: + { + // LOAD_CONST_12_2_3640336201 (specialized) + var dst = 12; + regs[base + dst] = this._constant(2, 3640336201); + break; + } + case 85: + { + // LOAD_CONST_7_3_2327532995 (specialized) + var dst = 7; + regs[base + dst] = this._constant(3, 2327532995); + break; + } + case 86: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 87: + { + // LOAD_GLOBAL_15_4_2360351562 (specialized) + var dst = 15; + var globalName = this._constant(4, 2360351562); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 88: + { + // LOAD_INT_9_3894990599 (specialized) + var dst = 9; + regs[base + dst] = 3894990599; + break; + } + case 89: + { + // LOAD_INT_10_240 (specialized) + var dst = 10; + regs[base + dst] = 240; + break; + } + case 90: + { + // LOAD_INT_11_11 (specialized) + var dst = 11; + regs[base + dst] = 11; + break; + } + case 91: + { + // LOAD_GLOBAL_17_4_2360351562 (specialized) + var dst = 17; + var globalName = this._constant(4, 2360351562); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 92: + { + // GET_PROP_19_17_4 (specialized) + // dst = regs[obj][regs[key]] + var dst = 19; + var obj = regs[base + 17]; + var key = regs[base + 4]; + regs[base + dst] = obj[key]; + break; + } + case 93: + { + // SET_PROP_15_3_17 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 15]; + var key = regs[base + 3]; + var val = regs[base + 17]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 94: + { + // RETURN_15 (specialized) + var retVal = regs[base + 15]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 95: + { + // LOAD_THIS_19 (specialized) + var dst = 19; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 96: + { + // LOAD_UPVALUE_14_0 (specialized) + var dst = 14; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 97: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 98: + { + // FINALLY_SETUP_3525_4_5_3349 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 3525, + contReg: 4, + payloadReg: 5, + throwPad: 3349 + }); + break; + } + case 99: + { + // TRY_SETUP_2990_3 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 2990, + exceptionReg: 3 + }); + break; + } + case 100: + { + // THROW_99 (specialized) + throw regs[base + 99]; + break; + } + case 101: + { + // RETURN_99 (specialized) + var retVal = regs[base + 99]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 102: + { + // LOAD_GLOBAL_7_113_3171167267 (specialized) + var dst = 7; + var globalName = this._constant(113, 3171167267); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 103: + { + // SET_PROP_11_12_13 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 11]; + var key = regs[base + 12]; + var val = regs[base + 13]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 104: + { + // JUMP_REG_4 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 4]; + break; + } + case 105: + { + // THROW_5 (specialized) + throw regs[base + 5]; + break; + } + case 106: + { + // GET_PROP_99_3_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 99; + var obj = regs[base + 3]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; + break; + } + case 107: + { + // LOAD_GLOBAL_11_4_2360351562 (specialized) + var dst = 11; + var globalName = this._constant(4, 2360351562); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 108: + { + // GET_PROP_89_89_92 (specialized) + // dst = regs[obj][regs[key]] + var dst = 89; + var obj = regs[base + 89]; + var key = regs[base + 92]; + regs[base + dst] = obj[key]; + break; + } + case 109: + { + // JUMP_REG_89 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 89]; + break; + } + case 110: + { + // LOAD_THIS_15 (specialized) + var dst = 15; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 111: + { + // GET_PROP_11_3_77 (specialized) + // dst = regs[obj][regs[key]] + var dst = 11; + var obj = regs[base + 3]; + var key = regs[base + 77]; + regs[base + dst] = obj[key]; + break; + } + case 112: + { + // RETURN_77 (specialized) + var retVal = regs[base + 77]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 113: + { + // JUMP_REG_67 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 67]; + break; + } + case 114: + { + // JUMP_REG_84 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 84]; + break; + } + case 115: + { + // LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 116: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 117: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 118: + { + // LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 119: + { + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 120: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 121: + { + // LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 122: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 123: + { + // LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 124: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 125: + { + // LOAD_THIS,LOAD_CONST (macro) + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 126: + { + // GET_PROP,LOAD_CONST (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 127: + { + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 128: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 129: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 130: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 131: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 132: + { + // LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 133: + { + // LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 134: + { + // GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 135: + { + // LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 136: + { + // LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 137: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 138: + { + // LOAD_INT,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 139: + { + // LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 140: + { + // LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 141: + { + // ALIAS_LOAD_INT_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + break; + } + case 142: + { + // ALIAS_LOAD_CONST_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 143: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 144: + { + // ALIAS_LOAD_GLOBAL_2_1_0 (order: [2,1,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 145: + { + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 146: + { + // ALIAS_GET_PROP_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + // dst = regs[obj][regs[key]] + var dst = _operands[0]; + var obj = regs[base + _operands[1]]; + var key = regs[base + _operands[2]]; + regs[base + dst] = obj[key]; + break; + } + case 61: + { + // ANTI_MOVE_6_4_5_2_0_3_7_1 (order: [6,4,5,2,0,3,7,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[6]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[5]]); + // MOVE + var dst = _operands[6]; + regs[base + dst] = regs[base + _operands[7]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_13_2_0_5_10_8_9_11_6_4_7_3_12_1 (order: [13,2,0,5,10,8,9,11,6,4,7,3,12,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[13], _unsortedOperands[1], _unsortedOperands[11], _unsortedOperands[9], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[12], _unsortedOperands[0]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this._constant(_operands[1], _operands[2]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // UNARY_NEG + var dst = _operands[3]; + regs[base + dst] = -regs[base + _operands[4]]; + // GTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a >= regs[base + _operands[7]]; + // BXOR + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a ^ regs[base + _operands[10]]; + // NEQ + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a !== regs[base + _operands[13]]; + break; + } + case 63: + { + // ANTI_NEQ_8_4_2_6_11_9_3_7_10_0_5_1 (order: [8,4,2,6,11,9,3,7,10,0,5,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[11], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[4]]; + // UNARY_NEG + var dst = _operands[0]; + regs[base + dst] = -regs[base + _operands[1]]; + // MOD + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a % regs[base + _operands[4]]; + // NEQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a !== regs[base + _operands[7]]; + // UNARY_NEG + var dst = _operands[8]; + regs[base + dst] = -regs[base + _operands[9]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[10]; + var target = _operands[11]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 64: + { + // ANTI_UNARY_NOT_7_10_2_4_11_3_5_0_9_1_6_8 (order: [7,10,2,4,11,3,5,0,9,1,6,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[4]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // TYPEOF + var dst = _operands[2]; + regs[base + dst] = typeof regs[base + _operands[3]]; + // UNARY_NOT + var dst = _operands[4]; + regs[base + dst] = !regs[base + _operands[5]]; + // GT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a > regs[base + _operands[8]]; + // MUL + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a * regs[base + _operands[11]]; + break; + } + case 65: + { + // ANTI_UNARY_POS_2_5_4_3_0_1_6 (order: [2,5,4,3,0,1,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[6]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // UNARY_POS + var dst = _operands[2]; + regs[base + dst] = +regs[base + _operands[3]]; + // LOAD_CONST + var dst = _operands[4]; + regs[base + dst] = this._constant(_operands[5], _operands[6]); + break; + } + case 66: + { + // ANTI_SUB_8_7_9_11_2_3_1_10_6_0_5_4 (order: [8,7,9,11,2,3,1,10,6,0,5,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[11], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[3]]; + // UNARY_NEG + var dst = _operands[0]; + regs[base + dst] = -regs[base + _operands[1]]; + // UNARY_POS + var dst = _operands[2]; + regs[base + dst] = +regs[base + _operands[3]]; + // UNARY_POS + var dst = _operands[4]; + regs[base + dst] = +regs[base + _operands[5]]; + // LOOSE_NEQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a != regs[base + _operands[8]]; + // SUB + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a - regs[base + _operands[11]]; + break; + } + case 67: + { + // ANTI_MUL_5_6_2_3_0_4_10_1_9_8_7 (order: [5,6,2,3,0,4,10,1,9,8,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[6]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // LOOSE_NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a != regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + // UNARY_POS + var dst = _operands[9]; + regs[base + dst] = +regs[base + _operands[10]]; + break; + } + case 68: + { + // ANTI_ADD_4_9_7_12_3_5_6_1_2_8_0_10_11 (order: [4,9,7,12,3,5,6,1,2,8,0,10,11]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[1], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[3]]; + // LT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; + // TYPEOF + var dst = _operands[3]; + regs[base + dst] = typeof regs[base + _operands[4]]; + // JUMP_IF_FALSE + var src = _operands[5]; + var target = _operands[6]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // LOOSE_NEQ + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a != regs[base + _operands[9]]; + // ADD + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a + regs[base + _operands[12]]; + break; + } + case 69: + { + // ANTI_EQ_8_2_5_3_1_11_0_6_9_10_4_7 (order: [8,2,5,3,1,11,0,6,9,10,4,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[10], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[5]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this._constant(_operands[1], _operands[2]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[3]; + var name = this._constant(_operands[4], _operands[5]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // DIV + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a / regs[base + _operands[8]]; + // EQ + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a === regs[base + _operands[11]]; + break; + } + case 70: + { + // ANTI_BAND_5_0_10_9_1_3_4_7_8_11_6_2 (order: [5,0,10,9,1,3,4,7,8,11,6,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[9]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // LTE + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a <= regs[base + _operands[5]]; + // BAND + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a & regs[base + _operands[8]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[11]]); + break; + } + case 71: + { + // ANTI_LT_3_11_1_6_5_4_9_2_8_7_10_0_12 (order: [3,11,1,6,5,4,9,2,8,7,10,0,12]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[11], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[12]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[3]; + var pairCount = _operands[4]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[5]]; + var val = regs[base + _operands[6]]; + o[key] = val; + } + regs[base + dst] = o; + // LT + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a < regs[base + _operands[9]]; + // LT + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a < regs[base + _operands[12]]; + break; + } + case 72: + { + // ANTI_SHL_10_5_6_11_8_3_2_1_9_0_7_12_4 (order: [10,5,6,11,8,3,2,1,9,0,7,12,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[12], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[10], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[11]]; + // SHL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a << regs[base + _operands[2]]; + // UNARY_NOT + var dst = _operands[3]; + regs[base + dst] = !regs[base + _operands[4]]; + // UNARY_POS + var dst = _operands[5]; + regs[base + dst] = +regs[base + _operands[6]]; + // SHL + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a << regs[base + _operands[9]]; + // EQ + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a === regs[base + _operands[12]]; + break; + } + case 73: + { + // ANTI_BOR_0_7_1_6_8_4_3_2_5 (order: [0,7,1,6,8,4,3,2,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[4]]; + // BOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a | regs[base + _operands[2]]; + // SHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >> regs[base + _operands[5]]; + // LT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a < regs[base + _operands[8]]; + break; + } + case 74: + { + // ANTI_BXOR_3_5_2_11_6_9_12_0_10_7_8_1_4 (order: [3,5,2,11,6,9,12,0,10,7,8,1,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[12], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[6]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // BOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a | regs[base + _operands[8]]; + // TYPEOF + var dst = _operands[9]; + regs[base + dst] = typeof regs[base + _operands[10]]; + // JUMP_IF_FALSE + var src = _operands[11]; + var target = _operands[12]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 75: + { + // ANTI_USHR_13_12_8_6_7_11_1_5_2_10_0_3_9_4 (order: [13,12,8,6,7,11,1,5,2,10,0,3,9,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[11], _unsortedOperands[13], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[12], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[0]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // USHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >>> regs[base + _operands[5]]; + // GTE + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >= regs[base + _operands[8]]; + // MOVE + var dst = _operands[9]; + regs[base + dst] = regs[base + _operands[10]]; + // LOOSE_EQ + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a == regs[base + _operands[13]]; + break; + } + case 76: + { + // ANTI_UNARY_BITNOT_3_6_4_5_2_12_10_14_7_13_1_0_9_8_11 (order: [3,6,4,5,2,12,10,14,7,13,1,0,9,8,11]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[11], _unsortedOperands[10], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[13], _unsortedOperands[12], _unsortedOperands[6], _unsortedOperands[14], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[7]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[3]; + var pairCount = _operands[4]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[5]]; + var val = regs[base + _operands[6]]; + o[key] = val; + } + regs[base + dst] = o; + // BOR + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a | regs[base + _operands[9]]; + // BOR + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a | regs[base + _operands[12]]; + // UNARY_BITNOT + var dst = _operands[13]; + regs[base + dst] = ~regs[base + _operands[14]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/qualification.json new file mode 100644 index 00000000..b96c5d81 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/qualification.json @@ -0,0 +1,79 @@ +{ + "id": "exceptions--stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodesAliasedOpcodes--109", + "case": "exceptions", + "profile": "stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodesAliasedOpcodes", + "seed": 109, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": true, + "specializedOpcodes": true, + "aliasedOpcodes": true, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true, + "concealConstants": true, + "antiInstrumentation": true, + "specializedOpcodes": true, + "macroOpcodes": true, + "aliasedOpcodes": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/eight-option-alias-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/encoded.js new file mode 100644 index 00000000..db458204 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/encoded.js @@ -0,0 +1,2371 @@ +function VM(fake_14, fake_13, constants, bytecode, globals, fake_12, fake_15) { + this.y = bytecode; + this.T = constants; + this.f = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.r = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.ad = fake_13; + this.h = fake_12 || fake_15; + this.j = []; + this.z = fake_14; + this.X = 1; + this.C = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(fake_1, fake_2, regs, absSlot, fake_3) { + this.M = fake_1 || fake_2; + this.j = regs; // shared reference to VM._regs flat array + this.V = fake_3; + this.e = absSlot; // absolute index; stable as long as frame is alive + this.c = false; + this.K = undefined; +} +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); +function decodeBytecode(s) { + if (!true) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} +// Closure & Frame +function Closure(fake_9, fn, fake_11, fake_10) { + this.ae = fn; + this.s = []; + this.prototype = {}; // <- default prototype object for `new` + this.E = fake_10; + this.n = fake_11; + this.A = fake_9; +} +var CONSTANTS = [/* 0 */-2073162450, /* 1 */"wFQzrKg9c5RsFA==", /* 2 */"YCMol2EToH5FBXmVJD16q0xnDvtNT3nF5EUZTavSDmeD4zAfm7IQBYyd7Aold5sbF6+POivVsk0L/ZdWBdPOv8Qi5Jbuff6NhAYmu7YvE5vu5uxtp93gRtRC+N/5c63mwGK2LQy29Bqoj/F7nJbQKsu98tWeUiX+hUMV1rsyAq7s5R167w6lm3gGkYoMHozlEWKA3gQyqEcfwnRObvIWWmyuUDqflxkKgH5qClGSej5vom1WLfJQTjPCRLZtLszaP2ZX6iseHooSBkueSvJzZpLjAq6JMkbGhEFq+pxWISpvvqsqEJaWeWSCpxY6sqcvZWKB9tJxitmSRu9K4t8MaqPmEpqvMf6G6gLlj8xz+ObbIre509G9ShP/hUtG1+Q6qKHHGfVy1A7PgxA3pbIaGZvhRWmw3ghLiUcf25JuqfkTYa+uAjOfhxYDmn4dEZSpfCFdq2tXW/s3T1/ZnLEXKaTiZ3dO8zsfToEoCVqRROk1Z2XbDk/dOH/BbUlz8RpnRSNiv0kTJomlAQaJlhAnu/wvAFuQ8CdJEsGeSHezvt8wY8ToT5GkCTuB+xj474l7luDXKM2xW8mYQCD/kVOiqN0g16n1Ecd4wADxm+035agzYLTp9HDU2N5D/TfH0Jlo1+FqGNiQHziThzkI53Bq6KehjDgy0LJHEcCvSD7QlLlCIKaYanBAiFMAVYhJMEOYbuAueIDTB0hCQEbYclBH+AtgVyhHsEoIYYCheQuQqBhhoE0oGVBi+FtAY9hOMDao2980aLHwfZi+ARu4gBBF6DBg9tgHUJ+5J8C7SADwnmsYoPg40pHiGeKA7AiIk104lqAJWLnx10jDwO9Lzje32N1gr/nsEZ2IEAOmh6YQqXj+4dap3TDoyIVHQ/uOUE7ZkqE/KbaQcXeIg6EYLrGMKQmh5OhAd4nbXU+8uCfRbWlx4VpnYRMmv78ANImkcSPpLidxu3svV1tlwWNJa9E2KVajhx8Cc2QJUIFiCRqxbxsS70d7ldYvSa5BMMm9Um3/jGMFp5wyiokWAb96FQ/6mxMnk6o8UZTJ/ULr3tWz8zfuovduwvEyGb+O7DvXl9Dq/X676fFRsz7YwrFHuPOIbjwi5LmrLs+a1QfBi/Ae5aqtISle3/IhdpbDIk7tsijW3GGK+iuWhgt4vqQKFZaeeQXilyYQsh3OZ0JCxmJyKNqvJkCqvx86ikoGQJppMk62ZCJp7lhyONYMQsI9PdZQaln+QmoOlks6Fo4YBpJyHe6NowUmlrI9XrjFHEpJ3rSrACapmjxuhfU+AqqOIjKtl93i5X6T1elVpD4byp9XB/rAbtslsbXODtWCrgf787Ye36LqJUZdtMoaR7Db306Uupol423o8jWXrQNDv78SIZWafWrtgVZru3rPt1ot9bFtId2CJkSzlB8Xg1kFeZ1oLW2lV1so7xJ7kcYDTcA9RNVSY2D/MBNylzo9bI1uFTh1Q++TWyI3K81uXUjFa3VW33WjASeanDINwoUlBbmMEzuqpynrdn2M1SNFjbw/05lXQOOCbBoVnrX2DPmcw3eS6+wcPaWHNSddx8zwR9PTvyywpMsly22x/P6ErgsKvNwU+eXOfcfs5lTzw5vP0VRq9K1tGJyqNGa08R8uhLX07p0vLPakJ1SezwFEn8RGTJg9bqR4ZG78UxRVhGk8QIwEFKBkG+xeVCs0X8xPXAj0VnRo3EGsOTSDnEMMpYViFGKsWjxopBfsSXztxS5EuLw2LKlnYPyWbCsUg4WuDCacjXc+7KdcRLTCNS5c6MwXdM7T0CzEpOAV3Y2I/CWEnAsfvKIk2uXofczM0UTaw+DP+1Tl5JltGJ2INMyL0R/qdIr0ip32Ley0X1O6zx5Dt9QYraI9IaWeFKX/DAuLhC09r60C5cBkR++6U2vHUs1OXRf1bGV9H9qrIjejnEwNefV9FWqvUyN0V0/Tbn1HxX9Vvr4CK0VnWft8jUIFV4VtDXOTsWc+45ReQbWmxWtdkMoUd7XbsS4atZYVMY2lAh6XgxNgv4oigNX9fcrKwkb8O++v3VoT5Z5t/ZLzBteD2R/4csvm7F39Lce2HVqIzzB62NbirfQ90ZbBEs7/NwK4llUywq0H5od6CM73WyfGzMq4XX3ml2I7Ho6zFgaigi8OqvVDGkmuUDpKVnXKXH7Z1QlSnL5qIi2WRfJqjm0CWboxLkCad2ZD6r5eJMpKwipeS/JxZhHifC4vsoYZMo6K+iSWhCszvrwqH9YwfqvCaVbiMwium2IJ9sNxApr3BseL+R/rauDm0VkAMqfGFkLHz9tyq+b5odY5+5bCCtP/8AvClj85laFG2e9y8c/oQ5S2wjLmWQ/hh2oyH7CLGAf9mjdxhPkHYSqvqTMvR5HCOHnE0T0puL4CK06Xd/sYgW8ZT7ErKT/jpncr8zVZfsF/yUlRa+tIZ1ibUA9ZucoBIImi8D1nUCNDvxNRE8lfQWnJGVeYOxqvxxgV8awJDoGFCDSzFR+ZYAnplFE4yYHAR1ud7yF76eDlqZAx1YjhAPH/AhOVqCYh2ano0Mt44s+1W8mw/Sj6Yc/ppnA5GLSD3Aj+kM5p0+HLWO3QlDsSQIXIMnCl6QAgv7h8EI+HRQAmiI8RPLiIIDbYnHcuyO5ACMhEsAhYb+BAeH+QIwgygMMZDZBc+GtgWihJsFBIVMBAeEzQfFjEIyWovRBf+GsAXZgXMGCoeeCdaH7wylkUwLs4LFCs7zRglhiokQU4koAzCP3/R2i7oFU47lHq2MRA2s+UU5243iChmT/whYi0H8aLyzDpmKphz/myUA==", /* 3 */"nTtKTqnaw0XazdlSl6bUIw==", /* 4 */"85XQ7eBi9+b/UwU/", /* 5 */-1858820092, /* 6 */925219696, /* 7 */undefined, /* 8 */1835943349, /* 9 */"58LiXv31y2X1HvSq7zfwmw==", /* 10 */"xJv+J+2o0FDPybBA", /* 11 */1904139107, /* 12 */"U5FReUyBUgs=", /* 13 */1641148362, /* 14 */"OT1PrQ7VZHN/zyBD", /* 15 */851055953, /* 16 */1649680259, /* 17 */-2090043355, /* 18 */"bD9Uq0pnuvxCDYuFNgVyj2AT", /* 19 */-1108910095, /* 20 */-604729469, /* 21 */"ykzCPNLX/WP4/CQVy5XZPLiMpRc=", /* 22 */-11817443, /* 23 */"V5mZMEevmWM=", /* 24 */"vBsUcL34", /* 25 */478907732, /* 26 */"LiipUWP4vUAU2KA3Dqiz4A==", /* 27 */284302430, /* 28 */"WfZcYkWqWjYjmpQP", /* 29 */"K7buIl1uaPJplgECnr0zFg==", /* 30 */453624958, /* 31 */"He+TVKvFxs2mXA==", /* 32 */578885406, /* 33 */-1600491895, /* 34 */"gRfg45l4U9A=", /* 35 */-1648727951, /* 36 */"vkWiNPvclWP759Uc7Yw=", /* 37 */2042846100, /* 38 */"p4sf/LhkTyR0vGJM", /* 39 */1354081998, /* 40 */"eEFmz4FzNdc=", /* 41 */"N0iC8XRYj6A6OKGXGAg=", /* 42 */1089604774, /* 43 */"dz5dqk4WWfo=", /* 44 */-1079536276, /* 45 */566575481, /* 46 */"TbNTJxGjZWmC8R0ZpIAEO4qX", /* 47 */375877287, /* 48 */"dyd1o3rZe3E=", /* 49 */"sEcN3IRs", /* 50 */1354458074, /* 51 */"gjbJgsoJyPGmGhWu", /* 52 */453611180, /* 53 */-1084635754, /* 54 */"ASGSqSgRnHgBA5+H", /* 55 */190360739, /* 56 */"pBYegrwNJpaJ+hturNsstoJK", /* 57 */"G9aLSTpCa9a9UwT+iGI3Fg==", /* 58 */-60248318, /* 59 */"qnUdnXorLLc=", /* 60 */-1435222112, /* 61 */-333277216, /* 62 */"Gee9k1IPrYI=", /* 63 */"fVle0Uy7TSdm6w==", /* 64 */1372212312, /* 65 */"FrGbSVbBX3+/0w==", /* 66 */1765640744, /* 67 */"ZHugzjlZgjEiqe5inOfpc4+e", /* 68 */"nK5dZaj1Ww2YgjMHKrM=", /* 69 */630742258, /* 70 */"GyOcpzUzy1w=", /* 71 */"rdscd67IIkFmyXsxOF8=", /* 72 */-615623766, /* 73 */"V9dDvYhN", /* 74 */1457399685, /* 75 */"tye7urvRplnFwpVO", /* 76 */-1493849001, /* 77 */"n8c5U7b+12XsJZ++", /* 78 */"cd1ddW71uF4dKw==", /* 79 */883686171, /* 80 */"M973QiD2tVMmLg==", /* 81 */true, /* 82 */"BNx/THc0bqQ1bID7EhSXjFg8", /* 83 */"IviYAeuY+zDBqw==", /* 84 */1628639794, /* 85 */1184566938, /* 86 */-1276567617, /* 87 */"nikVYZ76KG/U2+lGxErB0Q==", /* 88 */"pUoctireamJA5kSSSQ5wgg==", /* 89 */469534120, /* 90 */"LwaMcSXq4KYnOqGvwFo=", /* 91 */-1326671830, /* 92 */"/UC82MBxJ+nYIA==", /* 93 */805057574, /* 94 */2144651758, /* 95 */"b9eUa8zgPWmtkQ==", /* 96 */"oy4xstnX9kLA/uxV6tU=", /* 97 */-513239170, /* 98 */"KzmkoTUZ+PbiCw==", /* 99 */-1627639930, /* 100 */"U8ebShQ9i9UMZb/+DBOpl7kCC40=", /* 101 */-1974221178, /* 102 */"rGELKfW2+1vvz4R6", /* 103 */1608579406, /* 104 */-1977602421, /* 105 */"y4TIDfKdijTupNNfrOvKdA==", /* 106 */941704785, /* 107 */"Jwn9cQoZvKBjN7zT2kg=", /* 108 */2012834915, /* 109 */"9Z/J6/FmzNq+sQ==", /* 110 */1164232951, /* 111 */2018210491, /* 112 */"tm1W5U1T", /* 113 */1364563121, /* 114 */1142325123, /* 115 */"KyR3tLBNN9zERD/0a1wZJA==", /* 116 */"WeFid2nzGR9hgw==", /* 117 */1914526722, /* 118 */"mk0YxdNENU/608xk4ew=", /* 119 */1874141315, /* 120 */"W71PDQGVceN1b03TQLc=", /* 121 */-878513602, /* 122 */"gB1KtkIqct4=", /* 123 */-233847901, /* 124 */"pgc+gMwIB5COeSTgS1hqsAXISEA=", /* 125 */-2074790807, /* 126 */"2rfgKute0uUzcoQeQ4I=", /* 127 */-1231765908, /* 128 */"uCT13bVt8PTiRJnPBbSkVOzt", /* 129 */1283074257, /* 130 */-563849062, /* 131 */"XQ5gkkw6Ra4=", /* 132 */"0Vf5yAjAv0kk0Lx4COD7Fw==", /* 133 */"", /* 134 */"5wkokZ5+oePnV9Oy2c7oQej5", /* 135 */"q9QrT6b7Ukc=", /* 136 */"AF+2y8lGx9r9sdw5sKI/b4jzMAc=", /* 137 */"Kxq+9TJupKISJg==", /* 138 */"gk0DOqbGJUrh/+9q3iaBvWcS", /* 139 */"XmBu2HqwYkiQwWZI", /* 140 */"/HzaFcSl0zzirMxXP/uZTDfVnrUWLbKc", /* 141 */"ADGYWfTA9Ug=", /* 142 */-1905207214, /* 143 */"NXuL7HYl", /* 144 */1919955073, /* 145 */"bihqsGoIfoB6+Jtw", /* 146 */1837958513, /* 147 */"BY7K8QSaiS7Suw==", /* 148 */"fCWI7BZ7/oc=", /* 149 */"tshaQMX5EVA=", /* 150 */1726745434, /* 151 */"z7HeKj2fjutYd4Ua3oHCOfaSgm4=", /* 152 */"ZwlBkQ4/XKNFZ0rzcklhwQ==", /* 153 */190867527, /* 154 */"fgdUgdwJN5GsOCavr1s=", /* 155 */-1692715691, /* 156 */"LLaK2jBuSPZGkkIOC4JbFo6S", /* 157 */"YjBBqE3gPXiL8TSYqAA=", /* 158 */-1625502072, /* 159 */"duh9oEU4f7B0GDSAggg=", /* 160 */-217932412, /* 161 */"9wu38BgYvKEbOJVQfsiEQw==", /* 162 */-1771754138, /* 163 */"l2RGJce9CE2zxAN7", /* 164 */499332383, /* 165 */"k3vGzKNE4cXhzYlc0+TtY8qfuTQ=", /* 166 */1750094190, /* 167 */"F0Km1SSusjo=", /* 168 */103380037, /* 169 */"P6lOJxlbSs9feybBnkkmsZMo", /* 170 */"97rBJ+Lr6n7cig==", /* 171 */-44094637, /* 172 */"spl7ENu4pycx6K1w", /* 173 */"/VLoxplBMtnmNnyrgh8=", /* 174 */347566502, /* 175 */"F9rrTtL5", /* 176 */1153137641, /* 177 */"rvKdZdLd1So=", /* 178 */1018986824, /* 179 */-1925304118, /* 180 */"0dfvfN3M4lXuNcKs+Bzq97YL", /* 181 */-680680256, /* 182 */"DVKCxwtCs957Mg==", /* 183 */1479988760, /* 184 */"VfQSFNqNFDyFlB0UqH9u5A==", /* 185 */"pZ09BKS0ORw=", /* 186 */-1086568678, /* 187 */"YB9Lc2jl", /* 188 */-499836125, /* 189 */"s3B9iJkDJLiuEDlor+E=", /* 190 */852193666, /* 191 */"Eg/3mzw3gKp7Ye7paXHP3g==", /* 192 */1287323068, /* 193 */"B35Q6m6WTzpvgq4G", /* 194 */-1328267217, /* 195 */"Q41qHVG1oCQTX1nr", /* 196 */2081886677, /* 197 */1065317193, /* 198 */"0JbmM0yPoQIXednxrprkJvWr9V8=", /* 199 */"YeJDekcWSoo=", /* 200 */"FCWLWhDvmXsLxw==", /* 201 */"dHFk20JHYUtU32pp", /* 202 */"1VfDItKuqhUt+g==", /* 203 */"Ya2pNVKF", /* 204 */1909848166, /* 205 */"MxUnrb86K6ez6zV/nMoaRYu9Ky0=", /* 206 */-253617043, /* 207 */"cg1klWIjvK9j0zZ1SM1PXQ==", /* 208 */"w9PbLsGi2xXJjdz61IY=", /* 209 */"gKZSOkuSUwY=", /* 210 */1360137603, /* 211 */"XGJaFkaygA46gqN5He6SGjCm", /* 212 */"m98Zc2flWh0=", /* 213 */"/SMFpowyF14=", /* 214 */910127777, /* 215 */-1222956270, /* 216 */-1419858996, /* 217 */-904008098, /* 218 */"pHuQxi5Nob0=", /* 219 */1620251710, /* 220 */"uP4mU4gnEqOqLiiVtgUhjQ==", /* 221 */"0OneEDq3hwMNn5bwJGnG4eqo3zA=", /* 222 */"5STElKl9CIw=", /* 223 */"Um1V81hXPsM=", /* 224 */1249492429, /* 225 */"lEQixJ29HyyQ5Bpk", /* 226 */"S2ab6jlekMk2wpBeFvLQdw==", /* 227 */-1647230170, /* 228 */"/fLHZsrhMSqFtgbalE8=", /* 229 */"jxKjvs8l+eWgfjbLpkcGOw==", /* 230 */1338785824, /* 231 */"FYlAE3u3aCN0XwfxQ0lDwQ==", /* 232 */"UihDoFtoIfB0GCqA1DsXkA==", /* 233 */"gjs6VK3MJ3yj1TFU", /* 234 */"iWY/+oFvy9rwRvdN", /* 235 */"WmxV3AJES0xY3Gx0xuU=", /* 236 */"HV+Eyhh1q90YLaq2", /* 237 */"qoUzGkBufvplZlwqUL6sRQvC", /* 238 */866136413, /* 239 */"AEC1STHQqWg94JUbHbC5OA==", /* 240 */"ugt9n6xqqeEbWbiyMzeDQyTP", /* 241 */"E8GSeBXTqy8=", /* 242 */-916156610, /* 243 */"JZeba0r/jFQD1Ww9ak1F135zRO8=", /* 244 */-562426953, /* 245 */"PmuW/yRruNY=", /* 246 */"dwZb8kceYqZiKhle", /* 247 */"10ATyboxelk=", /* 248 */1482479932, /* 249 */"NhlvcUj5RoNWDw==", /* 250 */591457614, /* 251 */-581733409, /* 252 */-1008823052, /* 253 */"jhcmk4t+5OU9VbO+EcuGRw==", /* 254 */"zrz1FN5liv3nbMbU/UPNTw==", /* 255 */-1079491422, /* 256 */"IaSPLFbc", /* 257 */-730734530, /* 258 */"0qrB59F77Q4Mhb0FEI2HEg==", /* 259 */"0FXw/fps75SlOw==", /* 260 */-941580444, /* 261 */"RWN//18VaYVpPbyK", /* 262 */"XvhyYEEo", /* 263 */"kdZBu/xP2Nr6dg==", /* 264 */1035738694, /* 265 */"4QuMhOcc2K0xJbmkT+8="]; +VM.prototype.N = function (fake_29, fp, fake_27, fake_28) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.r; + if (!uvs) return; // nothing in this program ever captured a local + this.Y = [fake_27, fake_28]; + var regs = this.j; + var hi = fp + regs[fp + 6]; + this.F = fake_29; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.W([], "k"); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.ag = function (fake_19, fake_18, fake_16, fake_17) { + this.k = [fake_18, fake_19]; + this.o = [fake_16, fake_17]; + return this.y[this.j[this.C + 0]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +Upvalue.prototype.S = function (fake_4) { + this.d = fake_4; + return this.c ? this.K : this.j[this.e]; +}; +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.x = function (fake_26, idxIn, fake_25, keyIn) { + var idx = idxIn ?? this.ag(89, null, {}, null); + var key = keyIn ?? this.ag({}, {}, "z", "y"); + var v = this.T[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + this.ab = fake_25; + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + this.J = fake_26; + return out; +}; +Upvalue.prototype.W = function (fake_8, fake_7) { + this.K = this.j[this.e]; + this.c = true; + this.B = [fake_7, fake_8]; +}; +VM.prototype.R = function (closure, retDst, args, fake_20, thisVal) { + var fn = closure.ae; + var regs = this.j; + var fp = this.X; + var size = 8 + fn.U; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 8; + regs[fp + 0] = fn.G; + regs[fp + 1] = this.C; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + this.D = fake_20; + if (typeof void 0 === "number") regs[fp + void 0] = fn.m; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.G; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.X = end; + if (args) { + if (fn.w) { + var restSlot = fn.m - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.U; a++) regs[base + a] = args[a]; + } + if (fn.m < fn.U) regs[base + fn.m] = args; + } + this.C = fp; +}; +VM.prototype.a = function (fake_22, slot, fake_24, fake_23, fp, fake_21) { + this.u = fake_21; + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.j[fp + 7] + slot; + var uvs = this.r || (this.r = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue("q", {}, this.j, absSlot, "z")); + this.Q = [fake_22, fake_23]; + this.ac = fake_24; +}; +Upvalue.prototype.g = function (fake_6, fake_5, v) { + this.i = fake_6; + if (this.c) this.K = v;else this.j[this.e] = v; + this.v = fake_5; +}; +VM.prototype.q = function (fake_30, fake_31, closure, fake_32, args, thisVal) { + var now = () => { + return performance.now(); + }; + this.L = fake_32; + var lastTime = now(); + this.R(closure, 0, args, "z", thisVal); + this.t = fake_31 && fake_30; + while (true) { + var fp = this.C; + var regs = this.j; + var bc = this.y; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.y[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.y.length; i++) this.y[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.X; i2++) this.j[i2] = undefined; + op = 39; + regs[fp + 0] = this.y.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + this[op](); + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.C; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.N(undefined, searchFrame, undefined, []); + this.X = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.C = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.H !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.H] = err; + regs[handledFrame + 0] = h.aa; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.O] = h.p; + regs[hBase + h.af] = err; + regs[handledFrame + 0] = h.P; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.X = handledFrame + regs[handledFrame + 6]; + this.C = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.C) return this.j[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var VMPrototype = VM.prototype; +VMPrototype[68] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_ADD_8_7_1_3_4_0_10_9_12_6_2_5_11 (order: [8,7,1,3,4,0,10,9,12,6,2,5,11]) + let _unsortedOperands = [this.ag(undefined, undefined, 47, undefined), this.ag(4, undefined, "x", "q"), this.ag([], {}, {}, "y"), this.ag("z", 61, undefined, undefined), this.ag(93, undefined, {}, "x"), this.ag(30, [], [], []), this.ag(77, "y", {}, "k"), this.ag(84, {}, null, []), this.ag(undefined, undefined, 37, []), this.ag(null, null, {}, []), this.ag(null, undefined, "k", 88), this.ag("y", undefined, undefined, "k"), this.ag([], null, [], "z")]; + let _operands = [_unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[9], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[12], _unsortedOperands[8]]; + // BOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a | regs[base + _operands[2]]; + // USHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >>> regs[base + _operands[5]]; + // ADD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a + regs[base + _operands[8]]; + // UNARY_NEG + var dst = _operands[9]; + regs[base + dst] = -regs[base + _operands[10]]; + // UNARY_NOT + var dst = _operands[11]; + regs[base + dst] = !regs[base + _operands[12]]; +}; +VMPrototype[70] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_BAND_0_7_9_6_8_3_4_2_5_11_1_12_10 (order: [0,7,9,6,8,3,4,2,5,11,1,12,10]) + let _unsortedOperands = [this.ag(undefined, {}, "x", undefined), this.ag("k", "z", undefined, "z"), this.ag(9, null, null, 91), this.ag({}, [], {}, []), this.ag(null, null, "x", undefined), this.ag(null, null, 65, []), this.ag(12, null, "q", 5), this.ag(null, null, {}, "k"), this.ag(undefined, [], null, "k"), this.ag([], 49, "x", []), this.ag([], null, "z", undefined), this.ag([], undefined, undefined, {}), this.ag(null, 19, [], {})]; + let _operands = [_unsortedOperands[0], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[12], _unsortedOperands[9], _unsortedOperands[11]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // SHR + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a >> regs[base + _operands[4]]; + // LOOSE_NEQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a != regs[base + _operands[7]]; + // MOVE + var dst = _operands[8]; + regs[base + dst] = regs[base + _operands[9]]; + // BAND + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a & regs[base + _operands[12]]; +}; +VMPrototype[120] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_GLOBAL,LOAD_CONST,GET_PROP (macro) + // LOAD_GLOBAL + var dst = this.ag(14, {}, undefined, {}); + var globalName = this.x(); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; + // LOAD_CONST + var dst = this.ag(undefined, 73, undefined, {}); + regs[base + dst] = this.x(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.ag(null, "z", {}, null); + var obj = regs[base + this.ag({}, [], undefined, {})]; + var key = regs[base + this.ag(undefined, 8, 35, null)]; + regs[base + dst] = obj[key]; +}; +VMPrototype[130] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this.ag(null, 0, null, undefined); + regs[base + dst] = this.x(); + // LOAD_CONST + var dst = this.ag(undefined, {}, null, 16); + regs[base + dst] = this.x(); + // LOAD_THIS + var dst = this.ag("z", [], [], undefined); + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[53] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.ag(2, 15, null, "k"); + var iter = regs[base + this.ag(undefined, {}, "z", 76)]; + var exitTarget = this.ag(null, "y", {}, 18); + if (iter.I >= iter.b.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.b[iter.I++]; + } +}; +VMPrototype[93] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // SET_PROP_14_3_16 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 14]; + var key = regs[base + 3]; + var val = regs[base + 16]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[113] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // JUMP_REG_73 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + 73]; +}; +VMPrototype[98] = function () { + var fp = this.C; + var regs = this.j; + // FINALLY_SETUP_3607_4_5_2992 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + P: 3607, + O: 4, + af: 5, + p: 2992 + }); +}; +VMPrototype[100] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // SET_PROP_11_12_13 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 11]; + var key = regs[base + 12]; + var val = regs[base + 13]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[108] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // GET_PROP_98_98_101 (specialized) + // dst = regs[obj][regs[key]] + var dst = 98; + var obj = regs[base + 98]; + var key = regs[base + 101]; + regs[base + dst] = obj[key]; +}; +VMPrototype[15] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(21, undefined, {}, undefined); + var a = regs[base + this.ag("z", "x", "y", {})]; + regs[base + dst] = a % regs[base + this.ag(25, [], [], null)]; +}; +VMPrototype[85] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[115] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this.ag(undefined, 27, 57, []); + regs[base + dst] = this.ag("y", undefined, {}, 98); + // LOAD_INT + var dst = this.ag("y", {}, null, undefined); + regs[base + dst] = this.ag(66, "q", [], "y"); +}; +VMPrototype[17] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag([], 79, undefined, 26); + var a = regs[base + this.ag([], undefined, [], "z")]; + regs[base + dst] = a | regs[base + this.ag(37, {}, null, 4)]; +}; +VMPrototype[33] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag("k", undefined, null, 83); + regs[base + dst] = +regs[base + this.ag([], 31, 42, null)]; +}; +VMPrototype[60] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // Math.pow instead of `**` + var dst = this.ag(null, null, 61, "y"); + var a = regs[base + this.ag({}, undefined, undefined, {})]; + regs[base + dst] = Math.pow(a, regs[base + this.ag(undefined, {}, null, {})]); +}; +VMPrototype[55] = function () { + var fp = this.C; + var regs = this.j; + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); +}; +VMPrototype[25] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(null, [], {}, {}); + var a = regs[base + this.ag("q", 26, "y", 25)]; + regs[base + dst] = a >= regs[base + this.ag(undefined, undefined, "z", undefined)]; +}; +VMPrototype[122] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this.ag(null, 64, null, null); + var globalName = this.x(); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; + // LOAD_CONST + var dst = this.ag(null, [], null, "k"); + regs[base + dst] = this.x(); +}; +VMPrototype[56] = function () { + // destPc, sliceStart, sliceEnd, key + var destPc = this.ag(14, null, "k", undefined); + var sliceStart = this.ag(null, null, 91, {}); + var sliceEnd = this.ag([], undefined, "y", "z"); + var pk = this.ag([], undefined, null, undefined) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.y[destPc + (pi - sliceStart)] = (this.y[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } +}; +VMPrototype[20] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(null, [], 68, []); + var a = regs[base + this.ag(undefined, [], "k", "q")]; + regs[base + dst] = a >> regs[base + this.ag("z", null, "k", [])]; +}; +VMPrototype[106] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_GLOBAL_7_83_2327201105 (specialized) + var dst = 7; + var globalName = this.x([], 83, {}, 2327201105); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; +}; +VMPrototype[36] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag("y", [], 69, 66); + regs[base + dst] = typeof regs[base + this.ag(7, undefined, {}, [])]; +}; +VMPrototype[127] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this.ag({}, [], {}, "k"); + var globalName = this.x(); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; + // LOAD_CONST + var dst = this.ag(22, "x", [], []); + regs[base + dst] = this.x(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.ag(undefined, "q", [], "x"); + var obj = regs[base + this.ag(undefined, [], {}, {})]; + var key = regs[base + this.ag("z", undefined, null, 34)]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.ag(null, 20, 65, null); + regs[base + dst] = this.x(); +}; +VMPrototype[49] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.ag(undefined, 64, null, 13); + var pairCount = this.ag(45, undefined, 68, "k"); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.ag("q", 34, [], "x")]; + var val = regs[base + this.ag("k", 11, {}, "y")]; + o[key] = val; + } + regs[base + dst] = o; +}; +VMPrototype[90] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST_15_5_2436147205 (specialized) + var dst = 15; + regs[base + dst] = this.x(null, 5, "y", 2436147205); +}; +VMPrototype[133] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this.ag(undefined, 83, {}, null); + regs[base + dst] = this.x(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.ag([], "z", {}, 48); + var receiver = regs[base + this.ag(undefined, undefined, undefined, undefined)]; + var callee = regs[base + this.ag("q", 24, 36, "y")]; + var argc = this.ag([], "x", null, "k"); + var args; + if (argc === 65535) { + args = regs[base + this.ag(null, [], {}, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.ag(null, {}, [], null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.R(closure, dst << 1, args, {}, receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[91] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_GLOBAL_16_4_3814006823 (specialized) + var dst = 16; + var globalName = this.x(undefined, 4, undefined, 3814006823); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; +}; +VMPrototype[118] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this.ag(undefined, 26, {}, {}); + regs[base + dst] = this.x(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.ag({}, [], "x", {}); + var obj = regs[base + this.ag(64, undefined, {}, null)]; + var key = regs[base + this.ag([], "z", "x", "k")]; + regs[base + dst] = obj[key]; +}; +VMPrototype[88] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_INT_10_170 (specialized) + var dst = 10; + regs[base + dst] = 170; +}; +VMPrototype[66] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_SUB_0_1_7_2_6_8_4_5_3 (order: [0,1,7,2,6,8,4,5,3]) + let _unsortedOperands = [this.ag({}, 78, [], {}), this.ag({}, null, null, {}), this.ag([], {}, 3, null), this.ag(21, {}, undefined, []), this.ag({}, null, "z", 4), this.ag([], [], {}, "x"), this.ag(27, [], [], "z"), this.ag("z", null, null, []), this.ag("q", "x", [], undefined)]; + let _operands = [_unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[5]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // SUB + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a - regs[base + _operands[5]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[6]; + var count = _operands[7]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[8]]; + regs[base + dst] = elems; +}; +VMPrototype[54] = function () { + var fp = this.C; + var regs = this.j; + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + aa: this.ag(null, {}, undefined, "z"), + H: this.ag(undefined, "y", [], undefined) + }); +}; +VMPrototype[132] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this.ag(47, undefined, 69, "z"); + regs[base + dst] = this.x(); + // LOAD_THIS + var dst = this.ag([], {}, null, []); + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[72] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_SHL_5_11_1_12_4_2_13_9_7_10_0_3_8_6 (order: [5,11,1,12,4,2,13,9,7,10,0,3,8,6]) + let _unsortedOperands = [this.ag([], null, "q", undefined), this.ag(80, null, "k", []), this.ag([], {}, "z", []), this.ag("q", [], null, []), this.ag({}, "x", null, 30), this.ag([], "z", null, "y"), this.ag({}, null, undefined, undefined), this.ag([], 14, null, 97), this.ag(49, "k", "k", undefined), this.ag([], {}, 50, null), this.ag(undefined, 91, {}, []), this.ag(null, null, "z", {}), this.ag([], undefined, undefined, []), this.ag({}, null, 68, 2)]; + let _operands = [_unsortedOperands[10], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[13], _unsortedOperands[8], _unsortedOperands[12], _unsortedOperands[7], _unsortedOperands[9], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[6]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[0]; + var count = _operands[1]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[2]]; + regs[base + dst] = elems; + // MOVE + var dst = _operands[3]; + regs[base + dst] = regs[base + _operands[4]]; + // LTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a <= regs[base + _operands[7]]; + // SHL + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a << regs[base + _operands[10]]; + // LT + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a < regs[base + _operands[13]]; +}; +VMPrototype[139] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ALIAS_LOAD_INT_1_0 (order: [1,0]) + let _unsortedOperands = [this.ag("k", "q", 60, undefined), this.ag([], [], [], undefined)]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = _operands[1]; +}; +VMPrototype[94] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // RETURN_14 (specialized) + var retVal = regs[base + 14]; + this.N(undefined, fp, "k", null); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.X = fp; + this.C = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[34] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(null, [], [], []); + regs[base + dst] = !regs[base + this.ag(undefined, "k", "k", 85)]; +}; +VMPrototype[97] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // THROW_107 (specialized) + throw regs[base + 107]; +}; +VMPrototype[14] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(65, {}, {}, 98); + var a = regs[base + this.ag("x", "y", null, null)]; + regs[base + dst] = a / regs[base + this.ag("x", [], 1, [])]; +}; +VMPrototype[107] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_GLOBAL_11_4_3814006823 (specialized) + var dst = 11; + var globalName = this.x({}, 4, {}, 3814006823); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; +}; +VMPrototype[69] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_EQ_4_0_1_2_7_5_6_3 (order: [4,0,1,2,7,5,6,3]) + let _unsortedOperands = [this.ag("y", null, {}, []), this.ag(83, [], 55, {}), this.ag(93, null, "z", []), this.ag({}, undefined, "q", undefined), this.ag(undefined, "y", [], undefined), this.ag([], "x", [], undefined), this.ag([], "y", 61, {}), this.ag([], "z", "y", null)]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[4]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // UNARY_BITNOT + var dst = _operands[3]; + regs[base + dst] = ~regs[base + _operands[4]]; + // BAND + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a & regs[base + _operands[7]]; +}; +VMPrototype[138] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.ag("y", null, "y", {}); + regs[base + dst] = this.x(); + // LOAD_THIS + var dst = this.ag(null, {}, null, {}); + regs[base + dst] = regs[fp + 3]; + // LOAD_CONST + var dst = this.ag("y", "y", "z", {}); + regs[base + dst] = this.x(); +}; +VMPrototype[96] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // THROW_5 (specialized) + throw regs[base + 5]; +}; +VMPrototype[105] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // GET_PROP_107_3_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 107; + var obj = regs[base + 3]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; +}; +VMPrototype[18] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(undefined, {}, "k", {}); + var a = regs[base + this.ag({}, [], undefined, "k")]; + regs[base + dst] = a ^ regs[base + this.ag(null, [], "q", "z")]; +}; +VMPrototype[45] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + var retVal = regs[base + this.ag(null, "q", 49, "q")]; + this.N(79, fp, "y", undefined); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.X = fp; + this.C = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[125] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this.ag({}, undefined, "k", undefined); + regs[base + dst] = this.x(); + // RETURN + var retVal = regs[base + this.ag({}, undefined, 72, 41)]; + this.N("x", fp, {}, {}); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.X = fp; + this.C = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[11] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag("z", "q", [], "x"); + var a = regs[base + this.ag(null, {}, undefined, "z")]; + regs[base + dst] = a + regs[base + this.ag(undefined, [], null, undefined)]; +}; +VMPrototype[26] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(null, {}, "y", undefined); + var a = regs[base + this.ag(1, {}, {}, null)]; + regs[base + dst] = a === regs[base + this.ag([], undefined, undefined, undefined)]; +}; +VMPrototype[46] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + throw regs[base + this.ag("x", undefined, {}, "k")]; +}; +VMPrototype[74] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_USHR_14_13_5_8_4_10_12_9_7_6_2_0_3_11_1 (order: [14,13,5,8,4,10,12,9,7,6,2,0,3,11,1]) + let _unsortedOperands = [this.ag(80, {}, {}, []), this.ag([], "k", 8, []), this.ag({}, 75, [], null), this.ag({}, [], null, {}), this.ag(null, "q", "k", null), this.ag({}, 14, [], {}), this.ag("q", [], {}, 37), this.ag(undefined, undefined, [], []), this.ag("y", undefined, {}, undefined), this.ag([], null, null, 13), this.ag(undefined, 48, 68, {}), this.ag(75, null, [], []), this.ag(66, "y", {}, undefined), this.ag(undefined, null, {}, 89), this.ag([], null, undefined, [])]; + let _operands = [_unsortedOperands[11], _unsortedOperands[14], _unsortedOperands[10], _unsortedOperands[12], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[13], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[0]]; + // LOOSE_NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a != regs[base + _operands[2]]; + // DIV + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a / regs[base + _operands[5]]; + // DIV + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a / regs[base + _operands[8]]; + // USHR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a >>> regs[base + _operands[11]]; + // LOAD_GLOBAL + var dst = _operands[12]; + var globalName = this.x("k", _operands[13], "y", _operands[14]); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; +}; +VMPrototype[67] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_MUL_3_9_4_6_5_7_2_10_0_8_1 (order: [3,9,4,6,5,7,2,10,0,8,1]) + let _unsortedOperands = [this.ag(null, "x", undefined, 41), this.ag(undefined, "x", "k", "q"), this.ag({}, undefined, 12, {}), this.ag(null, 92, null, undefined), this.ag({}, [], "x", "z"), this.ag(2, {}, "z", "z"), this.ag([], {}, [], []), this.ag(85, "k", {}, "x"), this.ag("q", 45, [], {}), this.ag(4, "k", {}, {}), this.ag([], {}, null, 74)]; + let _operands = [_unsortedOperands[8], _unsortedOperands[10], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[1], _unsortedOperands[7]]; + // LOAD_CONST + var dst = _operands[0]; + regs[base + dst] = this.x([], _operands[1], "k", _operands[2]); + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + // UNARY_BITNOT + var dst = _operands[9]; + regs[base + dst] = ~regs[base + _operands[10]]; +}; +VMPrototype[1] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(null, undefined, "x", []); + regs[base + dst] = this.ag(9, undefined, {}, null); +}; +VMPrototype[22] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag({}, {}, [], undefined); + var a = regs[base + this.ag(undefined, 37, 16, null)]; + regs[base + dst] = a < regs[base + this.ag("z", "y", [], {})]; +}; +VMPrototype[9] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.ag("k", "k", {}, null)]; + var key = regs[base + this.ag({}, null, [], 21)]; + var val = regs[base + this.ag(undefined, {}, [], null)]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[111] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // RETURN_81 (specialized) + var retVal = regs[base + 81]; + this.N("z", fp, "q", []); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.X = fp; + this.C = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[61] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_MOVE_1_0_7_9_3_6_13_8_2_12_4_11_5_10 (order: [1,0,7,9,3,6,13,8,2,12,4,11,5,10]) + let _unsortedOperands = [this.ag("y", 47, null, {}), this.ag(11, "q", null, []), this.ag([], 84, {}, []), this.ag(undefined, 81, 93, null), this.ag(undefined, "y", undefined, "z"), this.ag("z", undefined, null, 48), this.ag({}, {}, "z", undefined), this.ag({}, "x", [], null), this.ag([], {}, [], {}), this.ag(undefined, {}, null, []), this.ag(undefined, "q", {}, null), this.ag(58, {}, null, "k"), this.ag(undefined, undefined, null, null), this.ag(null, null, {}, null)]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[10], _unsortedOperands[12], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[13], _unsortedOperands[11], _unsortedOperands[9], _unsortedOperands[6]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // SUB + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a - regs[base + _operands[5]]; + // ADD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a + regs[base + _operands[8]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[9]; + var name = this.x(null, _operands[10], {}, _operands[11]); + var val = Object.prototype.hasOwnProperty.call(this.f, name) ? this.f[name] : undefined; + regs[base + dst] = typeof val; + // MOVE + var dst = _operands[12]; + regs[base + dst] = regs[base + _operands[13]]; +}; +VMPrototype[37] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag("k", "x", null, {}); + this.ag(null, 17, undefined, null); // consumes argument (intended) + // consumes argument (intended) + regs[base + dst] = undefined; +}; +VMPrototype[137] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.ag([], "x", [], "k"); + regs[base + dst] = this.x(); + // LOAD_CONST + var dst = this.ag(65, [], {}, {}); + regs[base + dst] = this.x(); + // LOAD_THIS + var dst = this.ag([], {}, [], undefined); + regs[base + dst] = regs[fp + 3]; + // LOAD_CONST + var dst = this.ag(42, "y", undefined, "z"); + regs[base + dst] = this.x(); +}; +VMPrototype[31] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.ag({}, 88, null, []); + var obj = regs[base + this.ag([], [], "k", "z")]; + regs[base + dst] = obj instanceof regs[base + this.ag(null, 81, {}, null)]; +}; +VMPrototype[128] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this.ag("k", {}, 4, null); + regs[base + dst] = this.x(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.ag(undefined, "q", {}, {}); + var obj = regs[base + this.ag(72, "q", "y", undefined)]; + var key = regs[base + this.ag(undefined, "x", 65, 26)]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.ag(undefined, [], [], 69); + regs[base + dst] = this.x(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.ag(undefined, undefined, {}, {}); + var receiver = regs[base + this.ag(null, undefined, null, undefined)]; + var callee = regs[base + this.ag("z", null, 74, {})]; + var argc = this.ag(23, null, [], null); + var args; + if (argc === 65535) { + args = regs[base + this.ag([], 27, 28, 61)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.ag("y", {}, [], 6)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.R(closure, dst << 1, args, {}, receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[84] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST_7_2_2002982425 (specialized) + var dst = 7; + regs[base + dst] = this.x(undefined, 2, null, 2002982425); +}; +VMPrototype[44] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.ag({}, "q", null, 99); + var callee = regs[base + this.ag(75, {}, undefined, undefined)]; + var argc = this.ag([], 51, null, []); + var args; + if (argc === 65535) { + args = regs[base + this.ag("z", 85, {}, [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.ag("z", null, {}, {})]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.R(closure, dst << 1 | 1, args, 65, Object.create(closure.prototype || null)); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } +}; +VMPrototype[5] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag("q", [], {}, 64); + regs[base + dst] = regs[base + this.ag({}, null, {}, null)]; +}; +VMPrototype[40] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + var src = this.ag(undefined, undefined, "q", "x"); + var target = this.ag({}, [], [], {}); + if (!regs[base + src]) regs[fp + 0] = target; +}; +VMPrototype[59] = function () { + var fp = this.C; + var regs = this.j; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + P: this.ag("k", 84, [], []), + O: this.ag([], undefined, {}, {}), + af: this.ag(null, 35, null, null), + p: this.ag({}, null, "x", []) + }); +}; +VMPrototype[112] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // GET_PROP_11_3_81 (specialized) + // dst = regs[obj][regs[key]] + var dst = 11; + var obj = regs[base + 3]; + var key = regs[base + 81]; + regs[base + dst] = obj[key]; +}; +VMPrototype[135] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_INT,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this.ag(66, 69, "y", 88); + regs[base + dst] = this.ag("x", 44, {}, 44); + // LOAD_INT + var dst = this.ag("y", {}, 77, undefined); + regs[base + dst] = this.ag(30, undefined, "k", "x"); + // LOAD_INT + var dst = this.ag([], {}, "z", {}); + regs[base + dst] = this.ag([], undefined, "z", {}); + // LOAD_INT + var dst = this.ag(8, {}, [], "y"); + regs[base + dst] = this.ag([], [], null, 6); +}; +VMPrototype[32] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(undefined, null, null, 22); + regs[base + dst] = -regs[base + this.ag([], null, [], null)]; +}; +VMPrototype[109] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // JUMP_REG_98 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + 98]; +}; +VMPrototype[3] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + var dst = this.ag([], "k", undefined, 3); + regs[base + dst] = regs[fp + 4].s[this.ag("z", 91, 63, null)].S(null); +}; +VMPrototype[38] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.ag({}, undefined, null, "y"); + var name = this.x(); + var val = Object.prototype.hasOwnProperty.call(this.f, name) ? this.f[name] : undefined; + regs[base + dst] = typeof val; +}; +VMPrototype[104] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[2] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(undefined, "x", null, null); + var globalName = this.x(); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; +}; +VMPrototype[57] = function () { + debugger; +}; +VMPrototype[8] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // dst = regs[obj][regs[key]] + var dst = this.ag(46, null, {}, null); + var obj = regs[base + this.ag(undefined, [], {}, 74)]; + var key = regs[base + this.ag(undefined, null, undefined, undefined)]; + regs[base + dst] = obj[key]; +}; +VMPrototype[131] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.ag(undefined, {}, {}, "x"); + var obj = regs[base + this.ag(undefined, null, 50, {})]; + var key = regs[base + this.ag({}, 35, undefined, "x")]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.ag(undefined, {}, [], {}); + regs[base + dst] = this.x(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.ag([], 78, null, {}); + var receiver = regs[base + this.ag(null, [], 62, "q")]; + var callee = regs[base + this.ag({}, undefined, 35, "q")]; + var argc = this.ag([], [], [], null); + var args; + if (argc === 65535) { + args = regs[base + this.ag(27, {}, [], {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.ag(undefined, null, 73, null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.R(closure, dst << 1, args, {}, receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[143] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this.ag(null, {}, null, null)]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[117] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this.ag([], "z", null, []); + regs[base + dst] = this.x(); + // LOAD_INT + var dst = this.ag("y", undefined, "k", 45); + regs[base + dst] = this.ag(null, [], null, 23); +}; +VMPrototype[16] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag("k", [], 0, null); + var a = regs[base + this.ag(undefined, {}, {}, null)]; + regs[base + dst] = a & regs[base + this.ag(null, "z", undefined, 88)]; +}; +VMPrototype[76] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_UNARY_BITNOT_8_7_6_4_12_10_3_5_2_9_0_13_1_11 (order: [8,7,6,4,12,10,3,5,2,9,0,13,1,11]) + let _unsortedOperands = [this.ag(1, [], null, 12), this.ag({}, "q", undefined, "k"), this.ag({}, "z", {}, 95), this.ag(18, [], 24, undefined), this.ag(undefined, [], null, null), this.ag("z", null, {}, "y"), this.ag([], 46, {}, "k"), this.ag({}, {}, undefined, undefined), this.ag(null, [], "k", undefined), this.ag("q", {}, {}, {}), this.ag([], undefined, "q", undefined), this.ag("y", "y", undefined, null), this.ag(undefined, "x", "y", undefined), this.ag(undefined, null, null, "y")]; + let _operands = [_unsortedOperands[10], _unsortedOperands[12], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[13], _unsortedOperands[4], _unsortedOperands[11]]; + // SHL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a << regs[base + _operands[2]]; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // ADD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a + regs[base + _operands[8]]; + // UNARY_BITNOT + var dst = _operands[9]; + regs[base + dst] = ~regs[base + _operands[10]]; + // NEQ + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a !== regs[base + _operands[13]]; +}; +VMPrototype[101] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // JUMP_REG_4 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + 4]; +}; +VMPrototype[114] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // JUMP_REG_84 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + 84]; +}; +VMPrototype[42] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.ag({}, [], undefined, 72); + var callee = regs[base + this.ag([], undefined, undefined, "x")]; + var argc = this.ag(undefined, {}, [], {}); + var args; + if (argc === 65535) { + args = regs[base + this.ag(undefined, {}, 77, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.ag({}, "q", {}, [])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.R(closure, dst << 1, args, null, this.f); + } else { + regs[base + dst] = callee.apply(null, args); + } +}; +VMPrototype[30] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(null, [], undefined, []); + var a = regs[base + this.ag(null, "q", null, undefined)]; + regs[base + dst] = a in regs[base + this.ag(undefined, null, undefined, {})]; +}; +VMPrototype[65] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_UNARY_POS_3_1_6_5_4_7_0_2 (order: [3,1,6,5,4,7,0,2]) + let _unsortedOperands = [this.ag({}, [], 82, undefined), this.ag("z", 80, undefined, 64), this.ag(49, {}, undefined, undefined), this.ag([], [], [], {}), this.ag("y", undefined, {}, null), this.ag(87, [], undefined, null), this.ag(null, [], "x", null), this.ag([], "x", null, {})]; + let _operands = [_unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[5]]; + // UNARY_POS + var dst = _operands[0]; + regs[base + dst] = +regs[base + _operands[1]]; + // USHR + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a >>> regs[base + _operands[4]]; + // ADD + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a + regs[base + _operands[7]]; +}; +VMPrototype[19] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(null, undefined, "z", undefined); + var a = regs[base + this.ag(null, {}, null, [])]; + regs[base + dst] = a << regs[base + this.ag(27, [], 35, null)]; +}; +VMPrototype[80] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_INT_18_8 (specialized) + var dst = 18; + regs[base + dst] = 8; +}; +VMPrototype[129] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.ag([], null, 37, "x"); + regs[base + dst] = this.x(); + // LOAD_CONST + var dst = this.ag(undefined, undefined, null, null); + regs[base + dst] = this.x(); + // LOAD_CONST + var dst = this.ag({}, null, null, []); + regs[base + dst] = this.x(); +}; +VMPrototype[58] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.ag("z", undefined, {}, 78)]; +}; +VMPrototype[77] = function () { + var fp = this.C; + var regs = this.j; + // JUMP_3638 (specialized) + regs[fp + 0] = 3638; +}; +VMPrototype[86] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_GLOBAL_14_4_3814006823 (specialized) + var dst = 14; + var globalName = this.x(89, 4, [], 3814006823); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; +}; +VMPrototype[95] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_THIS_19 (specialized) + var dst = 19; + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[141] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this.ag(null, undefined, undefined, "x")]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this.N("x", fp, null, "y"); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.X = fp; + this.C = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[64] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_UNARY_NOT_7_5_4_6_3_2_0_1 (order: [7,5,4,6,3,2,0,1]) + let _unsortedOperands = [this.ag(undefined, undefined, undefined, "z"), this.ag({}, undefined, {}, null), this.ag(undefined, "z", null, {}), this.ag(null, "q", [], 11), this.ag(null, {}, [], []), this.ag(null, 93, 33, undefined), this.ag("q", null, null, "q"), this.ag({}, "x", null, "q")]; + let _operands = [_unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[0]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // BXOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a ^ regs[base + _operands[5]]; + // UNARY_NOT + var dst = _operands[6]; + regs[base + dst] = !regs[base + _operands[7]]; +}; +VMPrototype[43] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.ag(null, {}, 46, null); + var receiver = regs[base + this.ag(undefined, [], null, null)]; + var callee = regs[base + this.ag("z", "y", {}, [])]; + var argc = this.ag(91, undefined, undefined, 28); + var args; + if (argc === 65535) { + args = regs[base + this.ag("q", [], undefined, undefined)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.ag(null, [], "y", undefined)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.R(closure, dst << 1, args, [], receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[82] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST_12_0_2221812181 (specialized) + var dst = 12; + regs[base + dst] = this.x([], 0, undefined, 2221812181); +}; +VMPrototype[10] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.ag(9, 9, [], undefined); + var obj = regs[base + this.ag({}, 6, undefined, [])]; + var key = regs[base + this.ag([], 67, [], null)]; + regs[base + dst] = delete obj[key]; +}; +VMPrototype[134] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.ag([], 64, null, null); + regs[base + dst] = this.x(); + // LOAD_CONST + var dst = this.ag(13, "x", [], {}); + regs[base + dst] = this.x(); + // LOAD_CONST + var dst = this.ag(undefined, 58, 73, "y"); + regs[base + dst] = this.x(); + // LOAD_THIS + var dst = this.ag("y", [], undefined, undefined); + regs[base + dst] = regs[fp + 3]; + // LOAD_CONST + var dst = this.ag(undefined, undefined, 79, undefined); + regs[base + dst] = this.x(); +}; +VMPrototype[102] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // RETURN_107 (specialized) + var retVal = regs[base + 107]; + this.N("x", fp, 3, []); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.X = fp; + this.C = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[73] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_BOR_2_8_10_7_4_1_13_5_6_3_12_0_9_11 (order: [2,8,10,7,4,1,13,5,6,3,12,0,9,11]) + let _unsortedOperands = [this.ag([], null, "y", 38), this.ag(undefined, {}, undefined, 60), this.ag(94, {}, 46, []), this.ag([], {}, null, null), this.ag(undefined, "y", [], undefined), this.ag({}, {}, 97, {}), this.ag([], [], null, null), this.ag(67, null, 91, null), this.ag(undefined, 8, "x", undefined), this.ag(null, {}, [], 36), this.ag({}, {}, 87, "z"), this.ag(undefined, undefined, null, "z"), this.ag({}, [], {}, []), this.ag({}, undefined, "k", null)]; + let _operands = [_unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[12], _unsortedOperands[2], _unsortedOperands[13], _unsortedOperands[10], _unsortedOperands[6]]; + // MUL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a * regs[base + _operands[2]]; + // NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a !== regs[base + _operands[5]]; + // BOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a | regs[base + _operands[8]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[9]; + var name = this.x(null, _operands[10], 77, _operands[11]); + var val = Object.prototype.hasOwnProperty.call(this.f, name) ? this.f[name] : undefined; + regs[base + dst] = typeof val; + // TYPEOF + var dst = _operands[12]; + regs[base + dst] = typeof regs[base + _operands[13]]; +}; +VMPrototype[81] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this.N({}, fp, null, null); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.X = fp; + this.C = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[83] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST_13_1_3405358017 (specialized) + var dst = 13; + regs[base + dst] = this.x({}, 1, null, 3405358017); +}; +VMPrototype[124] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // GET_PROP,LOAD_CONST (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.ag({}, [], undefined, "q"); + var obj = regs[base + this.ag(49, null, "x", [])]; + var key = regs[base + this.ag([], null, 88, null)]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.ag(null, 3, [], "z"); + regs[base + dst] = this.x(); +}; +VMPrototype[28] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(undefined, 45, {}, []); + var a = regs[base + this.ag({}, null, null, null)]; + regs[base + dst] = a == regs[base + this.ag([], [], undefined, "q")]; +}; +VMPrototype[63] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_NEQ_8_1_10_9_4_3_6_7_5_2_0 (order: [8,1,10,9,4,3,6,7,5,2,0]) + let _unsortedOperands = [this.ag(undefined, [], "x", {}), this.ag(undefined, "z", 8, []), this.ag("z", null, undefined, undefined), this.ag("y", "y", undefined, null), this.ag([], [], 68, {}), this.ag(null, 87, 87, {}), this.ag({}, undefined, "q", null), this.ag({}, null, undefined, null), this.ag({}, "q", 63, {}), this.ag({}, 1, {}, []), this.ag([], "x", {}, null)]; + let _operands = [_unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[2]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // UNARY_POS + var dst = _operands[3]; + regs[base + dst] = +regs[base + _operands[4]]; + // NEQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a !== regs[base + _operands[7]]; + // GT + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a > regs[base + _operands[10]]; +}; +VMPrototype[41] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // || short-circuit: if truthy, jump over RHS. + var src = this.ag("k", {}, undefined, []); + var target = this.ag(null, "y", "z", []); + if (regs[base + src]) regs[fp + 0] = target; +}; +VMPrototype[62] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_UNARY_NEG_5_0_3_2_7_6_4_1 (order: [5,0,3,2,7,6,4,1]) + let _unsortedOperands = [this.ag(null, null, [], 41), this.ag(20, undefined, 35, 10), this.ag(undefined, "x", [], []), this.ag({}, undefined, {}, "y"), this.ag("y", undefined, null, undefined), this.ag(undefined, {}, undefined, null), this.ag({}, null, {}, 52), this.ag(undefined, undefined, 48, 28)]; + let _operands = [_unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[4]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[2]]); + // UNARY_NEG + var dst = _operands[3]; + regs[base + dst] = -regs[base + _operands[4]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[7]]); +}; +VMPrototype[48] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // dst, count, [elemReg...] + var dst = this.ag({}, {}, [], []); + var count = this.ag({}, [], 3, "k"); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.ag("y", 96, {}, {})]; + regs[base + dst] = elems; +}; +VMPrototype[21] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag({}, {}, null, {}); + var a = regs[base + this.ag(undefined, undefined, 43, {})]; + regs[base + dst] = a >>> regs[base + this.ag(null, [], "x", undefined)]; +}; +VMPrototype[50] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // obj, key, fn + var obj = regs[base + this.ag(undefined, {}, 72, "q")]; + var key = regs[base + this.ag(null, null, "k", null)]; + var getterFn = regs[base + this.ag("k", "x", null, 41)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); +}; +VMPrototype[119] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this.ag(null, {}, "x", null); + regs[base + dst] = this.ag(undefined, undefined, null, 45); + // LOAD_INT + var dst = this.ag({}, [], {}, {}); + regs[base + dst] = this.ag(undefined, "x", undefined, {}); + // LOAD_INT + var dst = this.ag("y", undefined, [], null); + regs[base + dst] = this.ag(22, null, undefined, {}); +}; +VMPrototype[142] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ALIAS_LOAD_GLOBAL_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this.ag([], undefined, 16, null), this.ag("z", undefined, null, 75), this.ag({}, "q", [], [])]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + var dst = _operands[0]; + var globalName = this.x([], _operands[1], {}, _operands[2]); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; +}; +VMPrototype[79] = function () { + var fp = this.C; + var regs = this.j; + // JUMP_6386 (specialized) + regs[fp + 0] = 6386; +}; +VMPrototype[35] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag({}, {}, undefined, {}); + regs[base + dst] = ~regs[base + this.ag(54, [], null, 47)]; +}; +VMPrototype[110] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_THIS_15 (specialized) + var dst = 15; + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[29] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(null, undefined, 21, 8); + var a = regs[base + this.ag("q", null, 96, 88)]; + regs[base + dst] = a != regs[base + this.ag("x", [], undefined, {})]; +}; +VMPrototype[13] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag("z", [], {}, "k"); + var a = regs[base + this.ag({}, [], [], {})]; + regs[base + dst] = a * regs[base + this.ag({}, undefined, null, [])]; +}; +VMPrototype[27] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(null, 11, {}, 42); + var a = regs[base + this.ag([], undefined, "y", undefined)]; + regs[base + dst] = a !== regs[base + this.ag([], 67, undefined, 54)]; +}; +VMPrototype[121] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.ag("k", undefined, null, null); + regs[base + dst] = this.x(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.ag([], {}, "k", undefined); + var obj = regs[base + this.ag([], {}, undefined, null)]; + var key = regs[base + this.ag(undefined, null, "q", undefined)]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.ag("q", [], "y", undefined); + regs[base + dst] = this.x(); +}; +VMPrototype[52] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.ag([], {}, {}, 37); + var obj = regs[base + this.ag(null, undefined, undefined, [])]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + b: keys, + I: 0 + }; +}; +VMPrototype[23] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(31, "x", [], "y"); + var a = regs[base + this.ag(75, null, null, {})]; + regs[base + dst] = a > regs[base + this.ag(undefined, "x", "q", {})]; +}; +VMPrototype[6] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // globals[globalName] = regs[src] + this.f[this.x()] = regs[base + this.ag([], {}, 59, 72)]; +}; +VMPrototype[89] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_INT_11_11 (specialized) + var dst = 11; + regs[base + dst] = 11; +}; +VMPrototype[39] = function () { + var fp = this.C; + var regs = this.j; + regs[fp + 0] = this.ag([], undefined, [], {}); +}; +VMPrototype[87] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_INT_9_3998684061 (specialized) + var dst = 9; + regs[base + dst] = 3998684061; +}; +VMPrototype[144] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ALIAS_GET_PROP_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this.ag([], {}, null, "q"), this.ag({}, [], undefined, null), this.ag(60, undefined, undefined, [])]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + // dst = regs[obj][regs[key]] + var dst = _operands[0]; + var obj = regs[base + _operands[1]]; + var key = regs[base + _operands[2]]; + regs[base + dst] = obj[key]; +}; +VMPrototype[75] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_BXOR_11_2_13_1_7_10_9_3_8_5_4_6_12_0 (order: [11,2,13,1,7,10,9,3,8,5,4,6,12,0]) + let _unsortedOperands = [this.ag(undefined, [], [], {}), this.ag({}, null, 70, "q"), this.ag({}, [], {}, "x"), this.ag(40, undefined, undefined, {}), this.ag(50, undefined, undefined, undefined), this.ag([], [], undefined, {}), this.ag("k", [], [], []), this.ag([], null, {}, "q"), this.ag([], "y", "x", []), this.ag({}, [], [], []), this.ag([], {}, null, []), this.ag(null, {}, {}, 8), this.ag(undefined, undefined, "x", "k"), this.ag({}, [], "x", 42)]; + let _operands = [_unsortedOperands[13], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[9], _unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[12], _unsortedOperands[2]]; + // LTE + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a <= regs[base + _operands[2]]; + // BAND + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a & regs[base + _operands[5]]; + // BXOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a ^ regs[base + _operands[8]]; + // SHR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a >> regs[base + _operands[11]]; + // MOVE + var dst = _operands[12]; + regs[base + dst] = regs[base + _operands[13]]; +}; +VMPrototype[78] = function () { + var fp = this.C; + var regs = this.j; + // JUMP_9717 (specialized) + regs[fp + 0] = 9717; +}; +VMPrototype[103] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_UPVALUE_14_0 (specialized) + var dst = 14; + regs[base + dst] = regs[fp + 4].s[0].S(undefined); +}; +VMPrototype[140] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ALIAS_LOAD_CONST_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this.ag("y", null, undefined, "z"), this.ag(56, [], {}, []), this.ag(null, [], {}, 2)]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + var dst = _operands[0]; + regs[base + dst] = this.x(null, _operands[1], "q", _operands[2]); +}; +VMPrototype[7] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + var uvIdx = this.ag(undefined, [], {}, undefined); + regs[fp + 4].s[uvIdx].g(null, 10, regs[base + this.ag("z", null, "x", 12)]); +}; +VMPrototype[51] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // obj, key, fn + var obj = regs[base + this.ag(undefined, 2, 99, {})]; + var key = regs[base + this.ag(null, undefined, [], {})]; + var setterFn = regs[base + this.ag([], 85, 53, [])]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); +}; +VMPrototype[116] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this.ag(null, {}, null, {}); + regs[base + dst] = this.x(); + // LOAD_CONST + var dst = this.ag([], {}, [], {}); + regs[base + dst] = this.x(); +}; +VMPrototype[0] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag([], [], undefined, {}); + regs[base + dst] = this.x(); +}; +VMPrototype[24] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(12, [], [], undefined); + var a = regs[base + this.ag({}, "x", [], {})]; + regs[base + dst] = a <= regs[base + this.ag({}, {}, null, "y")]; +}; +VMPrototype[71] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // ANTI_LT_10_0_9_12_8_11_4_6_3_5_1_7_13_14_2 (order: [10,0,9,12,8,11,4,6,3,5,1,7,13,14,2]) + let _unsortedOperands = [this.ag(undefined, {}, 58, "z"), this.ag({}, 34, {}, 18), this.ag("z", "y", 64, []), this.ag(null, 35, {}, "q"), this.ag([], 8, "q", []), this.ag({}, [], null, {}), this.ag(null, 76, "y", {}), this.ag({}, "k", [], []), this.ag([], 95, undefined, "y"), this.ag([], null, 41, {}), this.ag("k", 64, {}, null), this.ag("q", null, 75, undefined), this.ag(null, null, 75, "y"), this.ag("q", null, null, null), this.ag({}, null, "z", {})]; + let _operands = [_unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[14], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[12], _unsortedOperands[13]]; + // LT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; + // LOAD_CONST + var dst = _operands[3]; + regs[base + dst] = this.x({}, _operands[4], undefined, _operands[5]); + // LT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a < regs[base + _operands[8]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[11]]); + // NEQ + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a !== regs[base + _operands[14]]; +}; +VMPrototype[92] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // GET_PROP_18_16_4 (specialized) + // dst = regs[obj][regs[key]] + var dst = 18; + var obj = regs[base + 16]; + var key = regs[base + 4]; + regs[base + dst] = obj[key]; +}; +VMPrototype[4] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + var dst = this.ag([], [], "q", []); + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[136] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this.ag(null, null, null, {}); + regs[base + dst] = this.x(); + // LOAD_CONST + var dst = this.ag({}, undefined, null, "k"); + regs[base + dst] = this.x(); + // LOAD_CONST + var dst = this.ag([], null, 34, "k"); + regs[base + dst] = this.x(); + // LOAD_THIS + var dst = this.ag(88, 12, [], {}); + regs[base + dst] = regs[fp + 3]; +}; +VMPrototype[12] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + var dst = this.ag(undefined, [], undefined, 8); + var a = regs[base + this.ag({}, 88, {}, undefined)]; + regs[base + dst] = a - regs[base + this.ag("q", "z", [], undefined)]; +}; +VMPrototype[99] = function () { + var fp = this.C; + var regs = this.j; + // TRY_SETUP_3279_3 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + aa: 3279, + H: 3 + }); +}; +VMPrototype[126] = function () { + var regs = this.j; + var base = regs[this.C + 7]; + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_GLOBAL + var dst = this.ag("z", [], null, {}); + var globalName = this.x(); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; + // LOAD_CONST + var dst = this.ag({}, [], "z", 99); + regs[base + dst] = this.x(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this.ag("x", "z", null, []); + var obj = regs[base + this.ag([], 70, 14, {})]; + var key = regs[base + this.ag(98, {}, {}, null)]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this.ag("k", {}, "q", "x"); + regs[base + dst] = this.x(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.ag(null, [], {}, 83); + var receiver = regs[base + this.ag([], [], {}, {})]; + var callee = regs[base + this.ag(null, undefined, {}, [])]; + var argc = this.ag([], null, undefined, []); + var args; + if (argc === 65535) { + args = regs[base + this.ag(undefined, undefined, 58, 59)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.ag(undefined, {}, "q", [])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.R(closure, dst << 1, args, null, receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[47] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.ag({}, [], "k", {}); + var startPc = this.ag([], null, "q", undefined); + var paramCount = this.ag([], {}, null, []); + var regCount = this.ag(undefined, 57, {}, 45); + var uvCount = this.ag(67, [], 29, 19); + var hasRest = this.ag(57, {}, [], {}); // 1 if last param is a rest element + + // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.ag([], {}, undefined, {}); + var uvIndex = this.ag("x", 80, 45, undefined); + uvDescs[i] = { + Z: isLocalRaw, + l: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + m: paramCount, + U: regCount, + G: startPc, + w: hasRest + }; + var closure = new Closure(undefined, fn, "q", 81); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.Z) { + closure.s.push(this.a({}, uvd.l, [], [], fp, undefined)); + } else { + closure.s.push(regs[fp + 4].s[uvd.l]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(undefined, [], self.T, self.y, self.f, "z", undefined).q(95, [], c, null, Array.prototype.slice.call(arguments), this == null ? self.f : this); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + // unified prototype for new/instanceof + regs[base + dst] = shell; +}; +VMPrototype[123] = function () { + var fp = this.C; + var regs = this.j; + var base = regs[fp + 7]; + // LOAD_THIS,LOAD_CONST (macro) + // LOAD_THIS + var dst = this.ag("x", 31, null, {}); + regs[base + dst] = regs[fp + 3]; + // LOAD_CONST + var dst = this.ag([], undefined, [], {}); + regs[base + dst] = this.x(); +}; +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM({}, "q", CONSTANTS, decodeBytecode("UgAAAFMAAAAvAAAABgAAAEEOAAABAAAAUwAAAAAAAAAAAAAAVAAAACoAAAAFAAAABgAAAAEAAAAHAAAALwAAAAgAAAAAGQAABAAAAF0AAAAAAAAAAAAAAFUAAAAvAAAADgAAAHAAAAABAAAAbAAAAAEAAAAAAAAAAQAAAAUAAAA9AAAADAAAAA0AAAANAAAADAAAAAwAAAANAAAADgAAAA0AAAANAAAAAgAAAA0AAACGvWEjDQAAAAMAAABWAAAAVwAAAFgAAABZAAAAKgAAAAMAAAAIAAAABAAAAAUAAAAJAAAACgAAAAsAAABaAAAAPgAAAAwAAAAMAAAAEAAAAA0AAAAMAAAADAAAAA8AAAANAAAAKgAAAA8AAAACAAAAAQAAABAAAACMAAAABgAAABAAAAB0uyU3KgAAABEAAAACAAAAAQAAABAAAABbAAAAdwAAAAkAAADOLEQTCgAAALYAAAALAAAACAAAACoAAAAEAAAACAAAAAQAAAAFAAAACQAAAAoAAAALAAAAXAAAADAAAAAQAAAAAwAAAA8AAAARAAAAEgAAAF0AAACMAAAABwAAAA4AAAAAAAAAXgAAAIEAAABqAAAACAAAAEfrbm1pAAAACQAAADuB2MhoAAAACgAAACIAE34vAAAAZAAAAPwlAAACAAAADAAAAAAAAAAAAAAAXwAAAIwAAAALAAAAFAAAAIkMfnE9AAAAaAAAAGgAAABoAAAAaQAAAGoAAABpAAAAFAAAAGkAAABoAAAAFQAAAGoAAAABx/ubaQAAAAwAAACMAAAADQAAABYAAADK79FhPQAAAGkAAABpAAAAaQAAAGgAAABqAAAAaQAAABYAAABpAAAAagAAABcAAABqAAAA77Q9ZGkAAAAOAAAAjAAAAA8AAAAYAAAArki6Mj8AAABpAAAAagAAAGoAAABpAAAAaQAAAGoAAAAVAAAAGAAAABkAAABqAAAAagAAAEAAAAAZAAAAaAAAAGkAAABmAAAAaAAAAGoAAABpAAAAaQAAAEEAAABpAAAAZgAAAGkAAABoAAAAaQAAAGkAAABmAAAAaQAAAHMAAABiAAAAKf/x/2cAAAD24j6tQgAAAGkAAABpAAAAAgAAAGoAAABqAAAAaAAAAGcAAABiAAAAZwAAAEMAAABoAAAAagAAAGgAAABnAAAAaQAAAGcAAACxa1RiaAAAAGoAAABmAAAAEAAAAEQAAABnAAAAYgAAAGkAAABoAAAAaQAAAGgAAABqAAAAaAAAAGgAAABiAAAAaAAAAGkAAABpAAAAiwAAAN2R9JdjAAAATQAAAIwAAAARAAAAGgAAABhubIM9AAAAagAAAGoAAABpAAAAaQAAAGgAAABoAAAAGgAAAGgAAABqAAAAFwAAAGgAAAAx0KGQagAAABIAAABFAAAAaQAAABsAAAAVAAAAFwAAAGgAAABoAAAAaAAAAGoAAABAAAAAGwAAAGoAAABqAAAAZgAAAGgAAABpAAAAagAAAGoAAABBAAAAagAAAGYAAABoAAAAaAAAAGgAAABoAAAAZgAAAGgAAABzAAAAYgAAAEHjRABnAAAAaMH670IAAABpAAAAagAAAAAAAABoAAAAagAAAGgAAABnAAAAYgAAAGcAAABDAAAAaQAAAGoAAABoAAAAZwAAAGgAAABnAAAAE+XnvWgAAABpAAAAZgAAABMAAABEAAAAZwAAAGIAAABoAAAAaAAAAGoAAABpAAAAaAAAAGkAAABqAAAAYgAAAGgAAABpAAAAaQAAAIsAAAALLNqZYwAAAE0AAABzAAAAYgAAAHSf78JjAAAAhY3nlE0AAACMAAAAFAAAABwAAACwyPTbRAAAABwAAAAXAAAAaQAAAGgAAABoAAAAaAAAAGgAAABoAAAAaAAAAB0AAABpAAAAaAAAAGoAAAA9AAAAaAAAAGgAAABpAAAAaAAAAGkAAABpAAAAHQAAAGgAAABqAAAAFwAAAGgAAAA30ZLiaQAAABUAAABFAAAAaQAAAB4AAAAVAAAAFwAAAGgAAABoAAAAagAAAGkAAABAAAAAHgAAAGgAAABoAAAAZgAAAGoAAABoAAAAaQAAAGoAAABBAAAAaAAAAGYAAABoAAAAaAAAAGkAAABoAAAAZgAAAGkAAAA+AAAAaAAAAGkAAABmAAAAaAAAAGoAAABoAAAAZgAAAGkAAABzAAAAYgAAADySBVtnAAAAoGMV7EIAAABpAAAAaQAAAAEAAABpAAAAaAAAAGkAAABnAAAAYgAAAGcAAABGAAAAaAAAAGkAAABqAAAAaAAAAGkAAABoAAAAaQAAAGkAAABpAAAAZgAAAGgAAABnAAAAZwAAAEQAAABnAAAAYgAAAGoAAABqAAAAaQAAAGkAAABpAAAAaQAAAGgAAABiAAAAaQAAAGkAAABqAAAAiwAAAHlmuNxjAAAATQAAAHMAAABiAAAA7y7xN2MAAAAtWdulTQAAAIwAAAAWAAAAHwAAAMjVS/9EAAAAHwAAABcAAABpAAAAaAAAAGkAAABpAAAAagAAAGoAAABpAAAAIAAAAGkAAABoAAAAaQAAAD0AAABpAAAAaQAAAGkAAABqAAAAagAAAGoAAAAgAAAAaQAAAGkAAAAXAAAAaQAAAHh31C5oAAAAFwAAAEUAAABqAAAAIQAAABUAAAAXAAAAaAAAAGgAAABpAAAAaAAAAEAAAAAhAAAAaQAAAGoAAABmAAAAagAAAGkAAABpAAAAaAAAAEEAAABoAAAAZgAAAGgAAABpAAAAagAAAGgAAABmAAAAaQAAAHMAAABiAAAAJ2BLOmcAAAA8QKxNQgAAAGoAAABpAAAAAQAAAGoAAABoAAAAagAAAGcAAABiAAAAZwAAAEMAAABqAAAAagAAAGkAAABnAAAAagAAAGcAAABBDI0VagAAAGoAAABmAAAAGAAAAEQAAABnAAAAYgAAAGoAAABoAAAAaAAAAGkAAABpAAAAaAAAAGgAAABiAAAAaAAAAGoAAABoAAAAiwAAAPTHcgljAAAATQAAAHMAAABiAAAAN+OHVWMAAAAvNmVgTQAAAIwAAAAZAAAAIgAAAN0wixxCAAAAagAAAGoAAAABAAAAagAAAGkAAABoAAAAFwAAACIAAAAjAAAAPQAAAGoAAABqAAAAaQAAAGgAAABoAAAAagAAACMAAABqAAAAagAAABcAAABqAAAAvLEvgmoAAAAaAAAARQAAAGoAAAAkAAAAFQAAABcAAABoAAAAaQAAAGgAAABpAAAAQAAAACQAAABqAAAAaAAAAGYAAABqAAAAaAAAAGkAAABoAAAAQQAAAGkAAABmAAAAaQAAAGkAAABpAAAAagAAAGYAAABqAAAAPgAAAGoAAABpAAAAZgAAAGkAAABqAAAAaQAAAGYAAABpAAAAcwAAAGIAAABpPXKSZwAAAFzzikJCAAAAagAAAGoAAAACAAAAaQAAAGoAAABqAAAAZwAAAGIAAABnAAAARgAAAGkAAABqAAAAagAAAGkAAABqAAAAagAAAGoAAABqAAAAaQAAAGYAAABpAAAAZwAAAGcAAABEAAAAZwAAAGIAAABpAAAAaAAAAGgAAABpAAAAagAAAGoAAABpAAAAYgAAAGoAAABpAAAAagAAAIsAAADiy2H0YwAAAE0AAABzAAAAYgAAAK34CpFjAAAA2IdKvE0AAACMAAAAGwAAACUAAAA9h/IQRAAAACUAAAAXAAAAagAAAGkAAABpAAAAagAAAGoAAABoAAAAaAAAACYAAABqAAAAaQAAAGgAAAA9AAAAagAAAGkAAABoAAAAaAAAAGoAAABoAAAAJgAAAGoAAABoAAAAFwAAAGgAAACdrNWbagAAABwAAABFAAAAagAAACcAAAAVAAAAFwAAAGgAAABqAAAAaQAAAGoAAABAAAAAJwAAAGoAAABqAAAAZgAAAGoAAABoAAAAaAAAAGgAAABBAAAAaQAAAGYAAABqAAAAagAAAGkAAABoAAAAZgAAAGkAAABzAAAAYgAAAO9GDrZnAAAA+5D1BUIAAABpAAAAagAAAAAAAABoAAAAaQAAAGkAAABnAAAAYgAAAGcAAABDAAAAagAAAGoAAABoAAAAZwAAAGkAAABmAAAA7I0BeGgAAABoAAAAZwAAAB0AAABEAAAAZwAAAGIAAABoAAAAaQAAAGkAAABoAAAAagAAAGoAAABpAAAAYgAAAGkAAABoAAAAaAAAAIsAAABlDtDhYwAAAE0AAABzAAAAYgAAAJ3bvGVjAAAAqBT+sU0AAACMAAAAHgAAACgAAADCywkbQgAAAGgAAABoAAAAAAAAAGgAAABoAAAAaAAAABcAAAAoAAAAKQAAAD0AAABoAAAAaQAAAGkAAABoAAAAaAAAAGoAAAApAAAAagAAAGgAAAAXAAAAagAAAMQTCa5oAAAAHwAAAEUAAABoAAAAKgAAABUAAAAXAAAAaAAAAGgAAABoAAAAaQAAAEAAAAAqAAAAaAAAAGkAAABmAAAAagAAAGoAAABqAAAAaQAAAEEAAABoAAAAZgAAAGoAAABqAAAAaQAAAGoAAABmAAAAaAAAAHMAAABiAAAAMwlx5mcAAABZ5ybWQgAAAGgAAABpAAAAAgAAAGkAAABpAAAAaAAAAGcAAABiAAAAZwAAAEMAAABpAAAAagAAAGgAAABnAAAAaAAAAGcAAACg0oEiagAAAGkAAABmAAAAIAAAAEQAAABnAAAAYgAAAGoAAABqAAAAagAAAGoAAABoAAAAagAAAGoAAABiAAAAagAAAGkAAABqAAAAiwAAAOsUry9jAAAATQAAAHMAAABiAAAAHMpVGWMAAAD6kTDzTQAAAIwAAAAhAAAAKwAAAA5emqBEAAAAKwAAABcAAABoAAAAaQAAAGoAAABqAAAAaQAAAGoAAABqAAAALAAAAGoAAABqAAAAaAAAAD0AAABoAAAAaQAAAGgAAABpAAAAaQAAAGkAAAAsAAAAagAAAGgAAAAXAAAAaAAAADhm0VppAAAAIgAAAEUAAABqAAAALQAAABUAAAAXAAAAaAAAAGgAAABqAAAAagAAAEAAAAAtAAAAagAAAGkAAABmAAAAagAAAGgAAABoAAAAaQAAAEEAAABoAAAAZgAAAGgAAABpAAAAaQAAAGoAAABmAAAAaQAAAD4AAABoAAAAaAAAAGYAAABpAAAAaAAAAGoAAABmAAAAaAAAAHMAAABiAAAA6R8eSmcAAAD0aQWaQgAAAGgAAABoAAAAAQAAAGoAAABpAAAAaQAAAGcAAABiAAAAZwAAAEYAAABoAAAAaAAAAGkAAABoAAAAaAAAAGgAAABpAAAAagAAAGgAAABmAAAAaAAAAGcAAABnAAAARAAAAGcAAABiAAAAaAAAAGkAAABoAAAAaQAAAGgAAABoAAAAaQAAAGIAAABpAAAAaQAAAGkAAACLAAAA/YzwXGMAAABNAAAAcwAAAGIAAACCnhz2YwAAABYNLbdNAAAAjAAAACMAAAAuAAAAjcC6nUIAAABoAAAAaAAAAAIAAABoAAAAaQAAAGkAAAAXAAAALgAAAC8AAAA9AAAAaQAAAGoAAABoAAAAaAAAAGgAAABpAAAALwAAAGoAAABoAAAAFwAAAGkAAAB4TSDyaAAAACQAAABFAAAAaQAAADAAAAAVAAAAFwAAAGgAAABqAAAAaAAAAGoAAABAAAAAMAAAAGoAAABoAAAAZgAAAGgAAABoAAAAaQAAAGkAAABBAAAAaAAAAGYAAABpAAAAaAAAAGkAAABoAAAAZgAAAGoAAAA+AAAAaAAAAGkAAABmAAAAaQAAAGgAAABoAAAAZgAAAGkAAABzAAAAYgAAAMytYytnAAAAJBeYo0IAAABpAAAAaQAAAAIAAABqAAAAagAAAGgAAABnAAAAYgAAAGcAAABGAAAAagAAAGgAAABpAAAAaAAAAGgAAABoAAAAagAAAGgAAABqAAAAZgAAAGgAAABnAAAAZwAAAEQAAABnAAAAYgAAAGgAAABpAAAAaAAAAGgAAABqAAAAagAAAGoAAABiAAAAagAAAGkAAABpAAAAiwAAAIfezJJjAAAATQAAAHMAAABiAAAAWzro0WMAAAAl//eUTQAAAIwAAAAlAAAAMQAAAN4ew3lEAAAAMQAAABcAAABoAAAAaQAAAGkAAABoAAAAaAAAAGoAAABpAAAAMgAAAGgAAABpAAAAaQAAAD0AAABoAAAAaQAAAGkAAABqAAAAaQAAAGkAAAAyAAAAaQAAAGkAAAAXAAAAaAAAAEIWSsJqAAAAJgAAAEUAAABpAAAAMwAAABUAAAAXAAAAaQAAAGoAAABpAAAAaAAAAEAAAAAzAAAAagAAAGkAAABmAAAAaAAAAGkAAABpAAAAaQAAAEEAAABpAAAAZgAAAGoAAABqAAAAaAAAAGgAAABmAAAAaQAAAD4AAABqAAAAaQAAAGYAAABpAAAAagAAAGkAAABmAAAAaAAAAHMAAABiAAAAyWrbbGcAAADUtMK8QgAAAGoAAABqAAAAAQAAAGgAAABpAAAAagAAAGcAAABiAAAAZwAAAEYAAABqAAAAaQAAAGoAAABqAAAAaQAAAGoAAABpAAAAagAAAGoAAABmAAAAaQAAAGcAAABnAAAARAAAAGcAAABiAAAAaQAAAGkAAABqAAAAagAAAGkAAABoAAAAagAAAGIAAABpAAAAaQAAAGkAAACLAAAALRaexmMAAABNAAAAcwAAAGIAAABpneXdYwAAAKS90EJNAAAAjAAAACcAAAA0AAAAELG1UEIAAABpAAAAaQAAAAIAAABpAAAAaQAAAGgAAAAXAAAANAAAADUAAAA9AAAAaAAAAGgAAABpAAAAagAAAGoAAABpAAAANQAAAGoAAABoAAAAFwAAAGkAAABQ+C9oaAAAACgAAABFAAAAagAAADYAAAAVAAAAFwAAAGkAAABoAAAAaQAAAGkAAABAAAAANgAAAGgAAABoAAAAZgAAAGoAAABoAAAAaAAAAGgAAABBAAAAaAAAAGYAAABqAAAAagAAAGgAAABqAAAAZgAAAGgAAABzAAAAYgAAAHXBZmpnAAAAgAtOukIAAABqAAAAaQAAAAEAAABqAAAAaQAAAGoAAABnAAAAYgAAAGcAAABDAAAAaQAAAGgAAABqAAAAZwAAAGgAAABmAAAAhTE4vmoAAABoAAAAZwAAACkAAABEAAAAZwAAAGIAAABoAAAAaQAAAGoAAABqAAAAaAAAAGgAAABqAAAAYgAAAGgAAABpAAAAaAAAAIsAAACkqljjYwAAAE0AAABzAAAAYgAAAJFUL3VjAAAAdvhbg00AAACMAAAAKgAAADcAAACVDfJAQgAAAGoAAABqAAAAAgAAAGoAAABoAAAAagAAABcAAAA3AAAAOAAAAD0AAABqAAAAagAAAGoAAABoAAAAagAAAGoAAAA4AAAAaAAAAGoAAAAXAAAAaAAAAPvV/89oAAAAKwAAAEUAAABqAAAAOQAAABUAAAAXAAAAaAAAAGoAAABqAAAAaAAAAEAAAAA5AAAAagAAAGkAAABmAAAAaQAAAGoAAABpAAAAaQAAAEEAAABoAAAAZgAAAGoAAABqAAAAaQAAAGkAAABmAAAAaAAAAHMAAABiAAAAWAob6WcAAACgfJ94QgAAAGoAAABqAAAAAAAAAGgAAABqAAAAaAAAAGcAAABiAAAAZwAAAEMAAABoAAAAaAAAAGgAAABnAAAAaQAAAGcAAAAJaae/agAAAGgAAABmAAAALAAAAEQAAABnAAAAYgAAAGoAAABpAAAAagAAAGgAAABqAAAAaAAAAGgAAABiAAAAaAAAAGgAAABoAAAAiwAAAOjM2ktjAAAATQAAAHMAAABiAAAASBELe2MAAACV2NrkTQAAAIwAAAAtAAAAOgAAANjRxSFEAAAAOgAAABcAAABqAAAAaAAAAGoAAABqAAAAaQAAAGoAAABqAAAAOwAAAGoAAABqAAAAaAAAAD0AAABqAAAAagAAAGoAAABpAAAAaQAAAGoAAAA7AAAAaAAAAGoAAAAXAAAAagAAAN7UfiFpAAAALgAAAEUAAABoAAAAPAAAABUAAAAXAAAAaQAAAGoAAABoAAAAaAAAAEAAAAA8AAAAagAAAGkAAABmAAAAagAAAGgAAABpAAAAagAAAEEAAABpAAAAZgAAAGgAAABqAAAAaAAAAGkAAABmAAAAagAAAD4AAABqAAAAaAAAAGYAAABoAAAAaAAAAGoAAABmAAAAaAAAAHMAAABiAAAAMCYYDGcAAADNH8XcQgAAAGoAAABqAAAAAAAAAGoAAABoAAAAagAAAGcAAABiAAAAZwAAAEYAAABoAAAAaAAAAGgAAABqAAAAaQAAAGoAAABpAAAAagAAAGkAAABmAAAAaQAAAGcAAABnAAAARAAAAGcAAABiAAAAaAAAAGkAAABqAAAAagAAAGoAAABqAAAAaQAAAGIAAABoAAAAaQAAAGkAAACLAAAAJcxyH2MAAABNAAAAcwAAAGIAAACccRnJYwAAADeNlZxNAAAAjAAAAC8AAAA9AAAAa8xnFkIAAABoAAAAaAAAAAAAAABoAAAAaAAAAGoAAAAXAAAAPQAAAD4AAAA9AAAAagAAAGoAAABqAAAAaAAAAGkAAABqAAAAPgAAAGoAAABoAAAAFwAAAGgAAAC0trfkaQAAADAAAABFAAAAagAAAD8AAAAVAAAAFwAAAGoAAABoAAAAaAAAAGoAAABAAAAAPwAAAGoAAABoAAAAZgAAAGoAAABqAAAAaAAAAGgAAABBAAAAagAAAGYAAABoAAAAaAAAAGkAAABoAAAAZgAAAGoAAABzAAAAYgAAAC0UpQVnAAAAOl6MVUIAAABoAAAAaAAAAAAAAABpAAAAaAAAAGgAAABnAAAAYgAAAGcAAABDAAAAaQAAAGoAAABqAAAAZwAAAGkAAABnAAAAMsQJcWoAAABpAAAAZgAAADEAAABEAAAAZwAAAGIAAABqAAAAagAAAGoAAABoAAAAaAAAAGkAAABoAAAAYgAAAGgAAABoAAAAaAAAAIsAAAC1xK6TYwAAAE0AAABzAAAAYgAAAKp3dQZjAAAAZbANiE0AAACMAAAAMgAAAEAAAABRE7tQRAAAAEAAAAAXAAAAagAAAGoAAABpAAAAagAAAGoAAABpAAAAaQAAAEEAAABqAAAAagAAAGkAAAA9AAAAagAAAGkAAABpAAAAaAAAAGkAAABqAAAAQQAAAGkAAABpAAAAFwAAAGoAAAAcXdVdaQAAADMAAABFAAAAaQAAAEIAAAAVAAAAFwAAAGkAAABqAAAAaAAAAGkAAABAAAAAQgAAAGgAAABpAAAAZgAAAGkAAABpAAAAaQAAAGkAAABBAAAAaQAAAGYAAABoAAAAagAAAGkAAABqAAAAZgAAAGoAAABzAAAAYgAAAPtfOGpnAAAA8eANfEIAAABoAAAAagAAAAEAAABoAAAAaAAAAGgAAABnAAAAYgAAAGcAAABDAAAAaQAAAGkAAABpAAAAZwAAAGgAAABnAAAAYK0JG2oAAABqAAAAZgAAADQAAABEAAAAZwAAAGIAAABpAAAAagAAAGoAAABpAAAAagAAAGgAAABpAAAAYgAAAGoAAABpAAAAaAAAAIsAAACFSl8CYwAAAE0AAABzAAAAYgAAAEUMzJ1jAAAAXcSgTE0AAACMAAAANQAAAEMAAAAwu1m/QgAAAGkAAABpAAAAAAAAAGkAAABpAAAAaAAAABcAAABDAAAARAAAAD0AAABqAAAAaQAAAGkAAABpAAAAagAAAGkAAABEAAAAaAAAAGkAAAAXAAAAaQAAAEMVvRdpAAAANgAAAEUAAABqAAAARQAAABUAAAAXAAAAaQAAAGgAAABqAAAAaQAAAEAAAABFAAAAaQAAAGoAAABmAAAAagAAAGkAAABoAAAAaQAAAEEAAABqAAAAZgAAAGkAAABpAAAAaAAAAGkAAABmAAAAaQAAAD4AAABqAAAAaAAAAGYAAABpAAAAagAAAGoAAABmAAAAagAAAHMAAABiAAAAHTMfz2cAAACDOXL+QgAAAGkAAABpAAAAAgAAAGoAAABpAAAAagAAAGcAAABiAAAAZwAAAEYAAABqAAAAaQAAAGoAAABoAAAAaQAAAGgAAABpAAAAaQAAAGkAAABnAAAAagAAAGYAAABnAAAARAAAAGcAAABiAAAAaQAAAGgAAABpAAAAaQAAAGgAAABpAAAAaQAAAGIAAABpAAAAagAAAGoAAACLAAAAyxu8hWMAAABNAAAAcwAAAGIAAACIVzPwYwAAAA53sFRNAAAAjAAAADcAAABGAAAAZ+NYC0QAAABGAAAAFwAAAGoAAABqAAAAagAAAGoAAABoAAAAaAAAAGgAAABHAAAAagAAAGoAAABpAAAAPQAAAGkAAABpAAAAaAAAAGkAAABpAAAAagAAAEcAAABoAAAAaAAAABcAAABpAAAAwCze92oAAAA4AAAARQAAAGoAAABIAAAAFQAAABcAAABqAAAAaAAAAGgAAABpAAAAQAAAAEgAAABqAAAAaAAAAGYAAABpAAAAaQAAAGkAAABpAAAAQQAAAGgAAABmAAAAaQAAAGoAAABpAAAAagAAAGYAAABpAAAAcwAAAGIAAAB6MnO9ZwAAAIZ8Wg1CAAAAaQAAAGkAAAABAAAAaQAAAGkAAABpAAAAZwAAAGIAAABnAAAAQwAAAGkAAABoAAAAaAAAAGcAAABqAAAAZwAAAGISEi1pAAAAaQAAAGYAAAA5AAAARAAAAGcAAABiAAAAaQAAAGgAAABpAAAAaAAAAGkAAABpAAAAaQAAAGIAAABpAAAAaAAAAGkAAACLAAAA7F0Y0GMAAABNAAAAcwAAAGIAAAAeZL5GYwAAAF3QQIhNAAAAjAAAADoAAABJAAAAIuFo/EQAAABJAAAAFwAAAGkAAABpAAAAagAAAGoAAABpAAAAaQAAAGoAAABKAAAAagAAAGoAAABoAAAAPQAAAGgAAABoAAAAaAAAAGgAAABpAAAAagAAAEoAAABpAAAAagAAABcAAABpAAAA1MvWx2gAAAA7AAAARQAAAGgAAABLAAAAFQAAABcAAABqAAAAaAAAAGgAAABoAAAAQAAAAEsAAABpAAAAaQAAAGYAAABpAAAAaAAAAGgAAABoAAAAQQAAAGoAAABmAAAAagAAAGoAAABoAAAAaAAAAGYAAABqAAAAcwAAAGIAAACwLH7wZwAAAPueAoBCAAAAaAAAAGgAAAAAAAAAaQAAAGkAAABoAAAAZwAAAGIAAABnAAAAQwAAAGoAAABpAAAAaAAAAGcAAABqAAAAZgAAAHmSdKppAAAAaQAAAGcAAAA8AAAARAAAAGcAAABiAAAAaQAAAGkAAABpAAAAagAAAGkAAABpAAAAagAAAGIAAABqAAAAaAAAAGgAAACLAAAAqJWt/GMAAABNAAAAcwAAAGIAAADKyygGYwAAAP7QtZ5NAAAAjAAAAD0AAABMAAAAFkMi7EIAAABpAAAAaQAAAAIAAABpAAAAaAAAAGkAAAAXAAAATAAAAE0AAAA9AAAAagAAAGoAAABpAAAAagAAAGgAAABpAAAATQAAAGgAAABqAAAAFwAAAGkAAADideaiagAAAD4AAABFAAAAaAAAAE4AAAAVAAAAFwAAAGgAAABpAAAAaAAAAGoAAABAAAAATgAAAGoAAABpAAAAZgAAAGoAAABpAAAAaQAAAGoAAABBAAAAaQAAAGYAAABpAAAAagAAAGkAAABqAAAAZgAAAGoAAABzAAAAYgAAACV33SxnAAAA/WPknkIAAABqAAAAagAAAAEAAABqAAAAaQAAAGoAAABnAAAAYgAAAGcAAABDAAAAagAAAGkAAABpAAAAZwAAAGkAAABnAAAAsdy042kAAABqAAAAZgAAAD8AAABEAAAAZwAAAGIAAABqAAAAagAAAGgAAABoAAAAaAAAAGoAAABqAAAAYgAAAGoAAABpAAAAagAAAIsAAACDzV0aYwAAAE0AAABzAAAAYgAAACgdqe5jAAAAuMp13k0AAACMAAAAQAAAAE8AAAB8McpRRAAAAE8AAAAXAAAAaQAAAGoAAABpAAAAaQAAAGgAAABoAAAAaQAAAFAAAABpAAAAagAAAGoAAAA9AAAAaAAAAGgAAABoAAAAaQAAAGkAAABpAAAAUAAAAGoAAABoAAAAFwAAAGgAAAAPfpoqaQAAAEEAAABFAAAAaQAAAFEAAAAVAAAAFwAAAGkAAABpAAAAagAAAGoAAABAAAAAUQAAAGoAAABqAAAAZgAAAGoAAABpAAAAaQAAAGkAAABBAAAAagAAAGYAAABpAAAAaQAAAGkAAABpAAAAZgAAAGoAAAA+AAAAagAAAGoAAABmAAAAaQAAAGkAAABpAAAAZgAAAGoAAABzAAAAYgAAAHTbEflnAAAAornH6EIAAABqAAAAaAAAAAAAAABqAAAAaQAAAGoAAABnAAAAYgAAAGcAAABGAAAAaQAAAGkAAABqAAAAaQAAAGgAAABpAAAAagAAAGoAAABqAAAAZwAAAGkAAABmAAAAZwAAAEQAAABnAAAAYgAAAGgAAABqAAAAaQAAAGkAAABqAAAAagAAAGkAAABiAAAAagAAAGgAAABoAAAAiwAAAGdlfkpjAAAATQAAAHMAAABiAAAAahhSpGMAAABKOj3mTQAAAIwAAABCAAAAUgAAAM7hPWlCAAAAagAAAGoAAAACAAAAagAAAGgAAABoAAAAFwAAAFIAAABTAAAAPQAAAGoAAABpAAAAagAAAGgAAABqAAAAaQAAAFMAAABqAAAAaQAAABcAAABoAAAAaWOXtmoAAABDAAAARQAAAGkAAABUAAAAFQAAABcAAABqAAAAagAAAGkAAABpAAAAQAAAAFQAAABpAAAAaQAAAGYAAABqAAAAagAAAGgAAABqAAAAQQAAAGgAAABmAAAAaAAAAGkAAABoAAAAaAAAAGYAAABoAAAAcwAAAGIAAABP8E1OZwAAAJdi0t1CAAAAaAAAAGkAAAABAAAAagAAAGgAAABoAAAAZwAAAGIAAABnAAAAQwAAAGkAAABpAAAAaAAAAGcAAABpAAAAZgAAAGvM555oAAAAaAAAAGcAAABEAAAARAAAAGcAAABiAAAAaAAAAGkAAABoAAAAaQAAAGkAAABoAAAAaAAAAGIAAABoAAAAaAAAAGgAAACLAAAALdiZGWMAAABNAAAAcwAAAGIAAAC2t/GGYwAAAHO7NGVNAAAAjAAAAEUAAABVAAAAJ72YJUQAAABVAAAAFwAAAGgAAABqAAAAaQAAAGgAAABoAAAAaAAAAGoAAABWAAAAaAAAAGkAAABpAAAAPQAAAGgAAABoAAAAagAAAGoAAABqAAAAagAAAFYAAABqAAAAaAAAABcAAABqAAAA1ULF9WoAAABGAAAARQAAAGoAAABXAAAAFQAAABcAAABoAAAAaQAAAGoAAABpAAAAQAAAAFcAAABqAAAAaAAAAGYAAABoAAAAaAAAAGoAAABqAAAAQQAAAGoAAABmAAAAaAAAAGgAAABpAAAAagAAAGYAAABpAAAAcwAAAGIAAADY2Q3WZwAAALL7V+ZCAAAAagAAAGoAAAACAAAAaAAAAGkAAABpAAAAZwAAAGIAAABnAAAAQwAAAGgAAABqAAAAaQAAAGcAAABqAAAAZwAAAOCnE+FqAAAAagAAAGYAAABHAAAARAAAAGcAAABiAAAAaQAAAGgAAABqAAAAaQAAAGgAAABpAAAAaAAAAGIAAABpAAAAaAAAAGoAAACLAAAAkS8TJGMAAABNAAAAcwAAAGIAAADzQxj4YwAAANIa6sRNAAAAjAAAAEgAAABYAAAAXP9O20IAAABpAAAAaQAAAAAAAABoAAAAaAAAAGkAAAAXAAAAWAAAAFkAAAA9AAAAaAAAAGkAAABpAAAAaAAAAGkAAABpAAAAWQAAAGkAAABoAAAAFwAAAGkAAACDh4kcaAAAAEkAAABFAAAAaQAAAFoAAAAVAAAAFwAAAGkAAABpAAAAaQAAAGkAAABAAAAAWgAAAGkAAABoAAAAZgAAAGgAAABpAAAAaQAAAGkAAABBAAAAaQAAAGYAAABoAAAAaAAAAGgAAABqAAAAZgAAAGoAAAA+AAAAaAAAAGgAAABmAAAAaQAAAGgAAABpAAAAZgAAAGkAAABzAAAAYgAAADtmQu9nAAAADYiM/0IAAABpAAAAaAAAAAEAAABpAAAAaQAAAGkAAABnAAAAYgAAAGcAAABGAAAAaQAAAGkAAABoAAAAaQAAAGoAAABpAAAAaQAAAGgAAABpAAAAZwAAAGgAAABmAAAAZwAAAEQAAABnAAAAYgAAAGoAAABoAAAAaQAAAGgAAABoAAAAaAAAAGgAAABiAAAAaAAAAGoAAABoAAAAiwAAAIBe6A5jAAAATQAAAHMAAABiAAAAbNdC72MAAAC0xVCkTQAAAIwAAABKAAAAWwAAAHsL3lZCAAAAaQAAAGkAAAAAAAAAaQAAAGkAAABoAAAAFwAAAFsAAABcAAAAPQAAAGkAAABpAAAAagAAAGkAAABqAAAAagAAAFwAAABqAAAAaQAAABcAAABpAAAAx0IptWkAAABLAAAARQAAAGkAAABdAAAAFQAAABcAAABqAAAAagAAAGgAAABoAAAAQAAAAF0AAABqAAAAagAAAGYAAABpAAAAagAAAGoAAABqAAAAQQAAAGgAAABmAAAAagAAAGkAAABqAAAAaQAAAGYAAABqAAAAPgAAAGgAAABqAAAAZgAAAGgAAABoAAAAagAAAGYAAABqAAAAcwAAAGIAAADQ1aaAZwAAABhIKxBCAAAAaAAAAGoAAAAAAAAAaAAAAGgAAABpAAAAZwAAAGIAAABnAAAARgAAAGgAAABoAAAAaQAAAGoAAABqAAAAaQAAAGgAAABpAAAAaQAAAGcAAABpAAAAZgAAAGcAAABEAAAAZwAAAGIAAABoAAAAaAAAAGkAAABqAAAAagAAAGoAAABqAAAAYgAAAGoAAABqAAAAaAAAAIsAAAAsxMY2YwAAAE0AAABzAAAAYgAAAHB5Dl9jAAAAYQ1tAU0AAACMAAAATAAAAF4AAAA0O/WmRAAAAF4AAAAXAAAAagAAAGkAAABoAAAAaQAAAGkAAABqAAAAagAAAF8AAABpAAAAagAAAGgAAAA9AAAAaAAAAGoAAABoAAAAaAAAAGgAAABoAAAAXwAAAGgAAABoAAAAFwAAAGgAAAAC/fG3agAAAE0AAABFAAAAaAAAAGAAAAAVAAAAFwAAAGgAAABqAAAAaAAAAGgAAABAAAAAYAAAAGgAAABoAAAAZgAAAGkAAABqAAAAaAAAAGoAAABBAAAAaAAAAGYAAABqAAAAaAAAAGgAAABqAAAAZgAAAGoAAABzAAAAYgAAAJiLC1FnAAAAxTR+okIAAABoAAAAaAAAAAIAAABoAAAAagAAAGgAAABnAAAAYgAAAGcAAABDAAAAaQAAAGoAAABoAAAAZwAAAGgAAABmAAAARPhhN2kAAABoAAAAZwAAAE4AAABEAAAAZwAAAGIAAABoAAAAaAAAAGkAAABoAAAAaAAAAGoAAABqAAAAYgAAAGgAAABoAAAAaAAAAIsAAAD8zxnjYwAAAE0AAABzAAAAYgAAAK3btjljAAAAkmhEVk0AAABzAAAAYgAAANK4sa9jAAAAKJontk0AAACLAAAAMg4AAAQAAAA3AAAAiwAAAPQkAABhAAAAQgAAAGoAAABqAAAAAQAAAGoAAABoAAAAaAAAABUAAABhAAAAFQAAAHMAAABiAAAA/IXkDGMAAACk4c/CTQAAAIsAAADI6wAAFQAAAGAAAABQAAAAKgAAAAgAAAAPAAAABAAAAA4AAAAQAAAAEQAAABIAAAAsAAAAawAAAAcAAAABAAAACAAAAGEAAACLAAAAZWoAAGEAAABEAAAAYQAAABUAAABqAAAAaQAAAGkAAABqAAAAagAAAGoAAABpAAAAFQAAAGgAAABpAAAAaQAAAHMAAABiAAAAWZ21O2MAAAAZdPT1TQAAAEcAAABoAAAAawAAAGkAAABqAAAAagAAAGkAAABPAAAAagAAAGkAAACm6as0AAAAAGkAAABqAAAAaQAAAAYAAABAAAAAawAAAGoAAABqAAAAZgAAAGkAAABqAAAAagAAAGoAAABBAAAAaQAAAGYAAABqAAAAagAAAGoAAABqAAAAZgAAAGoAAAA+AAAAaAAAAGoAAABmAAAAagAAAGoAAABoAAAAZgAAAGkAAABzAAAAYgAAANPCqFFnAAAACPq/dUIAAABoAAAAagAAAAIAAABpAAAAagAAAGoAAABnAAAAYgAAAGcAAABGAAAAagAAAGgAAABpAAAAagAAAGkAAABqAAAAagAAAGgAAABqAAAAZgAAAGoAAABnAAAAZwAAAEQAAABnAAAAYgAAAGkAAABpAAAAagAAAGgAAABqAAAAaQAAAGgAAABiAAAAagAAAGkAAABqAAAAiwAAAHU+feNjAAAATQAAAIsAAABJAgAAYQAAAEIAAABoAAAAaAAAAAIAAABpAAAAaAAAAGgAAAAVAAAAYQAAABUAAABzAAAAYgAAABRDwrdjAAAAPsKC1U0AAACLAAAAM0cAAGEAAABCAAAAaQAAAGkAAAABAAAAaQAAAGkAAABpAAAAFQAAAGEAAAAVAAAAcwAAAGIAAADFe1lTYwAAAJxfSxFNAAAAYgAAAGMAAAB1AAAABgAAAA0AAADK79FhYQAAADUtAABCAAAAaAAAAGkAAAAAAAAAaAAAAGgAAABpAAAAFQAAAGEAAAAVAAAAcwAAAGIAAAALi7j8YwAAADAkXplNAAAANwAAAHMAAAAEAAAA7wwAAGEAAACRQgAAQgAAAGoAAABqAAAAAgAAAGoAAABqAAAAagAAABUAAABhAAAAFQAAAHMAAABiAAAAzZWS4GMAAADGIKIrTQAAAD0AAABqAAAAaAAAAGkAAABpAAAAaQAAAGoAAAAJAAAAaQAAAGgAAAAFAAAAaQAAAPhzRkhpAAAAUAAAAIsAAACyDAAABAAAADcAAACLAAAAY5cAAGEAAABEAAAAYQAAABUAAABoAAAAaQAAAGoAAABqAAAAagAAAGgAAABoAAAAFQAAAGoAAABpAAAAaQAAAHMAAABiAAAAp6vF72MAAAC8lIzjTQAAAIsAAACHuQAAFQAAAFEAAABkAAAAZQAAAIsAAABujwAAYQAAAEQAAABhAAAAFQAAAGoAAABqAAAAagAAAGoAAABqAAAAaAAAAGgAAAAVAAAAagAAAGoAAABqAAAAcwAAAGIAAACUfUZXYwAAAAXbCbVNAAAAhwAAABUAAAD8SAAAEAAAAO3lfsoRAAAA0AAAABIAAAAHAAAAiwAAADwrAABhAAAARAAAAGEAAAAVAAAAagAAAGkAAABpAAAAagAAAGoAAABqAAAAagAAABUAAABpAAAAagAAAGoAAABzAAAAYgAAAKWkagljAAAAdU/9qU0AAACLAAAAvBsAABUAAACMAAAABwAAAGsAAAAAAAAAZgAAADcAAACLAAAA1eEAAGEAAABEAAAAYQAAABUAAABpAAAAaAAAAGkAAABpAAAAagAAAGoAAABqAAAAFQAAAGkAAABpAAAAaAAAAHMAAABiAAAARwz0SWMAAACOEDABTQAAAGcAAAAvAAAADwAAAAAZAAAEAAAAXQAAAAAAAAAAAAAAaAAAAIsAAABFTQAAYQAAAEIAAABqAAAAagAAAAEAAABpAAAAagAAAGoAAAAVAAAAYQAAABUAAABzAAAAYgAAAKpgHUdjAAAAuwSUN00AAABQAAAAKgAAAAwAAAAPAAAABAAAAA4AAAAQAAAAEQAAABIAAAB1AAAADQAAAFEAAAAAAAAAYQAAAItYAABEAAAAYQAAABUAAABpAAAAagAAAGkAAABpAAAAaQAAAGkAAABqAAAAFQAAAGgAAABpAAAAaQAAAHMAAABiAAAAVaLpWmMAAACMbAi5TQAAACoAAAAKAAAADwAAAAQAAAAOAAAAEAAAABEAAAASAAAAaQAAAD0AAABpAAAAaQAAAGkAAABoAAAAaAAAAGkAAABrAAAAagAAAGkAAAAFAAAAaQAAAKLZtTNpAAAAUgAAAIsAAAANZQAAYQAAAEQAAABhAAAAFQAAAGoAAABpAAAAaAAAAGkAAABoAAAAaAAAAGgAAAAVAAAAaQAAAGgAAABoAAAAcwAAAGIAAADU1zcuYwAAAAyu7K5NAAAAiwAAAAsPAABhAAAARAAAAGEAAAAVAAAAaQAAAGkAAABqAAAAaQAAAGkAAABqAAAAaQAAABUAAABpAAAAaQAAAGgAAABzAAAAYgAAALqNyX9jAAAAK76Ny00AAACMAAAABQAAAGsAAAAFqDSRRAAAAGsAAAAAAAAAagAAAGoAAABqAAAAagAAAGgAAABoAAAAagAAAAkAAABqAAAAagAAAGgAAAA3AAAAiwAAADk+AABhAAAAQgAAAGkAAABoAAAAAAAAAGkAAABpAAAAaQAAABUAAABhAAAAFQAAAHMAAABiAAAAR0+ggmMAAADVRP18TQAAAGoAAAB3AAAAEAAAAH5TtjkRAAAAwwAAAGEAAACIwgAARAAAAGEAAAAVAAAAaQAAAGoAAABqAAAAaQAAAGkAAABpAAAAaAAAABUAAABoAAAAagAAAGkAAABzAAAAYgAAAEdnz1VjAAAAjiXqCk0AAACLAAAAvA8AAGEAAABEAAAAYQAAABUAAABqAAAAagAAAGoAAABqAAAAaAAAAGkAAABqAAAAFQAAAGoAAABqAAAAaQAAAHMAAABiAAAAjyWVQmMAAAAG7kwBTQAAAIsAAADavAAAYQAAAEQAAABhAAAAFQAAAGoAAABoAAAAaAAAAGgAAABoAAAAaAAAAGgAAAAVAAAAaAAAAGgAAABqAAAAcwAAAGIAAADoVekoYwAAAFAujF5NAAAAiwAAAMVaAABhAAAAQgAAAGoAAABqAAAAAgAAAGoAAABoAAAAagAAABUAAABhAAAAFQAAAHMAAABiAAAAlwij92MAAABMhxK9TQAAAIsAAABRtAAAFQAAAGsAAAB3AAAAEAAAAM4sRBMRAAAAtgAAAGEAAACCsAAAQgAAAGgAAABoAAAAAAAAAGgAAABqAAAAaAAAABUAAABhAAAAFQAAAHMAAABiAAAANZ2vo2MAAADX7AeiTQAAAIsAAADzgAAAFQAAAFEAAAAqAAAAYgAAAGQAAAACAAAAYgAAAGMAAACLAAAAAAAAAGUAAABsAAAAbQAAAHQAAABPAAAAVAAAAKEXE2FQAAAAVQAAABVxm0YvAAAASwAAAF0nAAACAAAACwAAAAAAAAAAAAAAbgAAAIwAAABWAAAAEAAAAN9o6bM9AAAAUAAAAFAAAABPAAAATwAAAE8AAABQAAAAEAAAAFAAAABQAAAAEQAAAE8AAACjqD6DTwAAAFcAAACMAAAADQAAABIAAADK79FhPQAAAE8AAABPAAAATwAAAFAAAABPAAAAUAAAABIAAABPAAAATwAAABMAAABQAAAAuw4ZGk8AAABYAAAAjAAAAFkAAAAUAAAAzw38Gz8AAABPAAAAUAAAAE8AAABPAAAAUAAAAE8AAAARAAAAFAAAABUAAABQAAAAUAAAAEAAAAAVAAAATwAAAFAAAABNAAAAUAAAAFAAAABPAAAATwAAAEEAAABQAAAATQAAAE8AAABQAAAATwAAAE8AAABNAAAATwAAAHMAAABJAAAAEoz0SU4AAACHzn6sQgAAAE8AAABPAAAAAQAAAFAAAABQAAAAUAAAAE4AAABJAAAATgAAAEMAAABQAAAAUAAAAFAAAABOAAAATwAAAE4AAAC5lMvCUAAAAE8AAABNAAAAWgAAAEQAAABOAAAASQAAAE8AAABQAAAAUAAAAFAAAABQAAAATwAAAE8AAABJAAAAUAAAAFAAAABPAAAAiwAAACNInOFKAAAATwAAAIwAAABbAAAAFgAAAGYM7LA9AAAATwAAAE8AAABQAAAATwAAAFAAAABPAAAAFgAAAFAAAABPAAAAEwAAAFAAAACRPcLgTwAAAFwAAABFAAAAUAAAABcAAAARAAAAEwAAAE8AAABQAAAATwAAAFAAAABAAAAAFwAAAE8AAABPAAAATQAAAFAAAABPAAAATwAAAE8AAABBAAAATwAAAE0AAABQAAAATwAAAFAAAABQAAAATQAAAE8AAABzAAAASQAAAHmhf8NOAAAA36hgT0IAAABQAAAAUAAAAAEAAABQAAAAUAAAAFAAAABOAAAASQAAAE4AAABDAAAAUAAAAFAAAABQAAAATgAAAFAAAABNAAAAwkT8L08AAABPAAAATgAAAF0AAABEAAAATgAAAEkAAABQAAAATwAAAFAAAABQAAAATwAAAE8AAABQAAAASQAAAFAAAABQAAAATwAAAIsAAABHeDvcSgAAAE8AAABzAAAASQAAALxURflKAAAAk0iKek8AAACMAAAAXgAAABgAAACi4dR/RAAAABgAAAATAAAATwAAAE8AAABPAAAATwAAAFAAAABPAAAATwAAABkAAABPAAAATwAAAE8AAAA9AAAATwAAAFAAAABQAAAAUAAAAE8AAABPAAAAGQAAAFAAAABPAAAAEwAAAE8AAAAaQlmvTwAAAF8AAABFAAAATwAAABoAAAARAAAAEwAAAFAAAABQAAAAUAAAAFAAAABAAAAAGgAAAE8AAABPAAAATQAAAE8AAABQAAAATwAAAFAAAABBAAAAUAAAAE0AAABQAAAATwAAAE8AAABQAAAATQAAAE8AAABzAAAASQAAAG/ql3FOAAAA1fF4/UIAAABQAAAATwAAAAIAAABQAAAATwAAAE8AAABOAAAASQAAAE4AAABDAAAAUAAAAE8AAABQAAAATgAAAE8AAABOAAAAkEF+lE8AAABPAAAATQAAAGAAAABEAAAATgAAAEkAAABQAAAAUAAAAFAAAABQAAAATwAAAE8AAABPAAAASQAAAE8AAABQAAAATwAAAIsAAAAwOObJSgAAAE8AAABzAAAASQAAAIjerkpKAAAAwAhuNk8AAACMAAAAYQAAABsAAABJkmjhRAAAABsAAAATAAAATwAAAE8AAABPAAAATwAAAE8AAABPAAAATwAAABwAAABQAAAATwAAAE8AAAA9AAAATwAAAFAAAABPAAAAUAAAAE8AAABQAAAAHAAAAE8AAABPAAAAEwAAAE8AAADZVMQAUAAAAGIAAABFAAAAUAAAAB0AAAARAAAAEwAAAE8AAABQAAAAUAAAAFAAAABAAAAAHQAAAE8AAABQAAAATQAAAFAAAABPAAAAUAAAAE8AAABBAAAAUAAAAE0AAABQAAAATwAAAE8AAABPAAAATQAAAE8AAAA+AAAAUAAAAE8AAABNAAAAUAAAAE8AAABQAAAATQAAAFAAAABzAAAASQAAAOIg4OROAAAASCjBcEIAAABPAAAAUAAAAAEAAABQAAAATwAAAFAAAABOAAAASQAAAE4AAABGAAAATwAAAE8AAABPAAAAUAAAAFAAAABPAAAAUAAAAFAAAABPAAAATgAAAE8AAABNAAAATgAAAEQAAABOAAAASQAAAE8AAABPAAAATwAAAE8AAABPAAAATwAAAFAAAABJAAAATwAAAE8AAABPAAAAiwAAANdCKMhKAAAATwAAAHMAAABJAAAA9Xhu80oAAAD+Y4ZLTwAAAIwAAABjAAAAHgAAAG0P/J5CAAAATwAAAFAAAAAAAAAATwAAAE8AAABPAAAAEwAAAB4AAAAfAAAAPQAAAE8AAABPAAAAUAAAAE8AAABPAAAAUAAAAB8AAABPAAAATwAAABMAAABPAAAAu1hs5E8AAABkAAAARQAAAFAAAAAgAAAAEQAAABMAAABPAAAATwAAAE8AAABQAAAAQAAAACAAAABPAAAAUAAAAE0AAABQAAAATwAAAFAAAABPAAAAQQAAAE8AAABNAAAATwAAAFAAAABQAAAATwAAAE0AAABPAAAAPgAAAE8AAABQAAAATQAAAFAAAABQAAAAUAAAAE0AAABQAAAAcwAAAEkAAADYqQOzTgAAAD6x5D5CAAAAUAAAAFAAAAAAAAAAUAAAAE8AAABQAAAATgAAAEkAAABOAAAARgAAAE8AAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAE0AAABPAAAATgAAAE4AAABEAAAATgAAAEkAAABQAAAATwAAAE8AAABQAAAATwAAAE8AAABQAAAASQAAAFAAAABPAAAATwAAAIsAAAC3oKNjSgAAAE8AAABzAAAASQAAAGI+7T9KAAAAHZrqkk8AAACMAAAAZQAAACEAAAAGzVOKRAAAACEAAAATAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAACIAAABPAAAAUAAAAE8AAAA9AAAATwAAAE8AAABPAAAAUAAAAE8AAABQAAAAIgAAAFAAAABPAAAAEwAAAFAAAACQBv49TwAAAGYAAABFAAAAUAAAACMAAAARAAAAEwAAAFAAAABQAAAAUAAAAE8AAABAAAAAIwAAAFAAAABQAAAATQAAAFAAAABPAAAATwAAAE8AAABBAAAATwAAAE0AAABQAAAAUAAAAFAAAABPAAAATQAAAE8AAABzAAAASQAAAP+GS3FOAAAAZY4s/UIAAABQAAAAUAAAAAIAAABPAAAAUAAAAFAAAABOAAAASQAAAE4AAABDAAAATwAAAFAAAABPAAAATgAAAE8AAABNAAAA4kDgX1AAAABQAAAATgAAAGcAAABEAAAATgAAAEkAAABQAAAATwAAAE8AAABQAAAAUAAAAE8AAABPAAAASQAAAFAAAABQAAAATwAAAIsAAABvzB5BSgAAAE8AAABzAAAASQAAAByfpLhKAAAA2s6j6k8AAACMAAAAaAAAACQAAAAJICCKQgAAAE8AAABQAAAAAAAAAE8AAABPAAAATwAAABMAAAAkAAAAJQAAAD0AAABQAAAAUAAAAFAAAABQAAAAUAAAAE8AAAAlAAAATwAAAFAAAAATAAAAUAAAAHqHftJPAAAAaQAAAEUAAABQAAAAJgAAABEAAAATAAAATwAAAFAAAABPAAAAUAAAAEAAAAAmAAAAUAAAAFAAAABNAAAATwAAAFAAAABQAAAAUAAAAEEAAABQAAAATQAAAFAAAABPAAAATwAAAE8AAABNAAAATwAAAD4AAABPAAAATwAAAE0AAABQAAAATwAAAFAAAABNAAAATwAAAHMAAABJAAAAN96nqE4AAACd5Yg0QgAAAFAAAABQAAAAAgAAAE8AAABQAAAAUAAAAE4AAABJAAAATgAAAEYAAABPAAAATwAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABNAAAAUAAAAE4AAABOAAAARAAAAE4AAABJAAAATwAAAE8AAABQAAAAUAAAAE8AAABPAAAAUAAAAEkAAABQAAAAUAAAAFAAAACLAAAADVxhc0oAAABPAAAAcwAAAEkAAAA43oi2SgAAAE2LH+pPAAAAjAAAAGoAAAAnAAAA/B8hOEQAAAAnAAAAEwAAAE8AAABQAAAATwAAAE8AAABPAAAATwAAAE8AAAAoAAAATwAAAFAAAABPAAAAPQAAAE8AAABPAAAAUAAAAE8AAABPAAAATwAAACgAAABQAAAATwAAABMAAABPAAAA7w/mMVAAAABrAAAARQAAAE8AAAApAAAAEQAAABMAAABPAAAATwAAAE8AAABPAAAAQAAAACkAAABQAAAAUAAAAE0AAABPAAAATwAAAFAAAABPAAAAQQAAAE8AAABNAAAAUAAAAE8AAABPAAAATwAAAE0AAABQAAAAPgAAAE8AAABQAAAATQAAAE8AAABQAAAATwAAAE0AAABPAAAAcwAAAEkAAAAnF5JBTgAAAI0ec81CAAAAUAAAAE8AAAACAAAATwAAAFAAAABQAAAATgAAAEkAAABOAAAARgAAAE8AAABPAAAATwAAAFAAAABPAAAAUAAAAE8AAABPAAAAUAAAAE0AAABQAAAATgAAAE4AAABEAAAATgAAAEkAAABQAAAAUAAAAE8AAABQAAAATwAAAFAAAABQAAAASQAAAFAAAABPAAAATwAAAIsAAAA3ZAFMSgAAAE8AAABzAAAASQAAAJNjNOhKAAAA5b7w908AAACMAAAAbAAAACoAAABRRfl3QgAAAE8AAABPAAAAAAAAAE8AAABQAAAATwAAABMAAAAqAAAAKwAAAD0AAABQAAAAUAAAAFAAAABPAAAAUAAAAE8AAAArAAAAUAAAAFAAAAATAAAAUAAAAMOd4rJQAAAAbQAAAEUAAABPAAAALAAAABEAAAATAAAATwAAAFAAAABQAAAAUAAAAEAAAAAsAAAATwAAAFAAAABNAAAAUAAAAFAAAABQAAAAUAAAAEEAAABQAAAATQAAAFAAAABPAAAAUAAAAFAAAABNAAAATwAAAHMAAABJAAAA2aRqIk4AAAA/rEuuQgAAAE8AAABQAAAAAQAAAFAAAABQAAAAUAAAAE4AAABJAAAATgAAAEMAAABPAAAATwAAAFAAAABOAAAATwAAAE4AAAANUGRFTwAAAFAAAABNAAAAbgAAAEQAAABOAAAASQAAAFAAAABQAAAATwAAAE8AAABPAAAATwAAAFAAAABJAAAAUAAAAFAAAABPAAAAiwAAACCFAP9KAAAATwAAAHMAAABJAAAAEi6yZ0oAAAAwl8NQTwAAAIwAAABvAAAALQAAAEYkS3hCAAAAUAAAAFAAAAACAAAAUAAAAE8AAABQAAAAEwAAAC0AAAAuAAAAPQAAAE8AAABPAAAATwAAAE8AAABQAAAATwAAAC4AAABPAAAATwAAABMAAABQAAAAyhvTQE8AAABwAAAARQAAAFAAAAAvAAAAEQAAABMAAABPAAAATwAAAE8AAABPAAAAQAAAAC8AAABPAAAAUAAAAE0AAABQAAAATwAAAE8AAABQAAAAQQAAAE8AAABNAAAATwAAAE8AAABPAAAATwAAAE0AAABQAAAAcwAAAEkAAACi4NlcTgAAAAjouuhCAAAATwAAAE8AAAACAAAATwAAAE8AAABQAAAATgAAAEkAAABOAAAAQwAAAFAAAABQAAAATwAAAE4AAABPAAAATgAAABc2VVFPAAAATwAAAE0AAABxAAAARAAAAE4AAABJAAAAUAAAAFAAAABPAAAATwAAAE8AAABQAAAATwAAAEkAAABPAAAAUAAAAFAAAACLAAAA8vdoWEoAAABPAAAAcwAAAEkAAADevPJGSgAAALQV5g9PAAAAjAAAAHIAAAAwAAAASlAWREIAAABQAAAAUAAAAAAAAABPAAAAUAAAAFAAAAATAAAAMAAAADEAAAA9AAAAUAAAAFAAAABQAAAATwAAAE8AAABQAAAAMQAAAFAAAABPAAAAEwAAAFAAAADThTv9UAAAAHMAAABFAAAAUAAAADIAAAARAAAAEwAAAFAAAABPAAAAUAAAAFAAAABAAAAAMgAAAE8AAABPAAAATQAAAE8AAABQAAAATwAAAFAAAABBAAAAUAAAAE0AAABPAAAAUAAAAE8AAABQAAAATQAAAE8AAABzAAAASQAAANwSrx9OAAAAQhqQq0IAAABPAAAAUAAAAAAAAABQAAAATwAAAE8AAABOAAAASQAAAE4AAABDAAAATwAAAE8AAABPAAAATgAAAE8AAABOAAAAbUWQbU8AAABPAAAATQAAAHQAAABEAAAATgAAAEkAAABPAAAATwAAAE8AAABQAAAATwAAAFAAAABQAAAASQAAAE8AAABPAAAAUAAAAIsAAABTdGspSgAAAE8AAABzAAAASQAAANOY52JKAAAA130zK08AAACMAAAAdQAAADMAAACiWR1yRAAAADMAAAATAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAADQAAABQAAAAUAAAAE8AAAA9AAAAUAAAAFAAAABQAAAATwAAAE8AAABQAAAANAAAAE8AAABQAAAAEwAAAFAAAAADl3LNUAAAAHYAAABFAAAATwAAADUAAAARAAAAEwAAAFAAAABPAAAATwAAAE8AAABAAAAANQAAAFAAAABQAAAATQAAAFAAAABPAAAAUAAAAE8AAABBAAAAUAAAAE0AAABQAAAAUAAAAFAAAABQAAAATQAAAE8AAAA+AAAATwAAAFAAAABNAAAATwAAAE8AAABQAAAATQAAAE8AAABzAAAASQAAAKmPBhROAAAAD5fnn0IAAABQAAAAUAAAAAIAAABQAAAAUAAAAFAAAABOAAAASQAAAE4AAABGAAAAUAAAAE8AAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABPAAAATgAAAE8AAABNAAAATgAAAEQAAABOAAAASQAAAE8AAABQAAAAUAAAAFAAAABQAAAAUAAAAE8AAABJAAAAUAAAAFAAAABQAAAAiwAAAKA4nqZKAAAATwAAAHMAAABJAAAAvgE6D0oAAAAxJYWmTwAAAIwAAAB3AAAANgAAAGIotW9EAAAANgAAABMAAABQAAAATwAAAFAAAABPAAAATwAAAFAAAABPAAAANwAAAE8AAABPAAAATwAAAD0AAABPAAAATwAAAFAAAABQAAAATwAAAE8AAAA3AAAATwAAAFAAAAATAAAATwAAAPOpnvVPAAAAeAAAAEUAAABPAAAAOAAAABEAAAATAAAATwAAAE8AAABPAAAATwAAAEAAAAA4AAAATwAAAFAAAABNAAAATwAAAE8AAABQAAAATwAAAEEAAABQAAAATQAAAFAAAABQAAAATwAAAE8AAABNAAAATwAAAD4AAABQAAAAUAAAAE0AAABPAAAATwAAAFAAAABNAAAAUAAAAHMAAABJAAAAVGkyS04AAAC6cBPXQgAAAE8AAABPAAAAAgAAAE8AAABQAAAAUAAAAE4AAABJAAAATgAAAEYAAABQAAAATwAAAFAAAABPAAAAUAAAAFAAAABQAAAAUAAAAFAAAABNAAAATwAAAE4AAABOAAAARAAAAE4AAABJAAAATwAAAE8AAABQAAAAUAAAAE8AAABQAAAAUAAAAEkAAABPAAAAUAAAAFAAAACLAAAAD6D84EoAAABPAAAAcwAAAEkAAAA8yyVJSgAAAFQsGMlPAAAAjAAAAHkAAAA5AAAAW4aiy0QAAAA5AAAAEwAAAE8AAABQAAAAUAAAAE8AAABQAAAATwAAAFAAAAA6AAAATwAAAFAAAABQAAAAPQAAAE8AAABQAAAATwAAAE8AAABPAAAATwAAADoAAABPAAAATwAAABMAAABPAAAATXoEf1AAAAB6AAAARQAAAE8AAAA7AAAAEQAAABMAAABPAAAATwAAAE8AAABQAAAAQAAAADsAAABQAAAAUAAAAE0AAABQAAAAUAAAAFAAAABQAAAAQQAAAE8AAABNAAAATwAAAE8AAABPAAAAUAAAAE0AAABQAAAAPgAAAE8AAABQAAAATQAAAFAAAABPAAAAUAAAAE0AAABQAAAAcwAAAEkAAAA6n+msTgAAAKCmyjhCAAAAUAAAAFAAAAAAAAAATwAAAE8AAABQAAAATgAAAEkAAABOAAAARgAAAFAAAABQAAAAUAAAAFAAAABPAAAAUAAAAFAAAABQAAAAUAAAAE0AAABPAAAATgAAAE4AAABEAAAATgAAAEkAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAASQAAAFAAAABPAAAAUAAAAIsAAABDq8/wSgAAAE8AAABzAAAASQAAALiNBqFKAAAA94XZkE8AAACMAAAAewAAADwAAABU6Q/yRAAAADwAAAATAAAAUAAAAE8AAABPAAAAUAAAAFAAAABQAAAATwAAAD0AAABPAAAAUAAAAE8AAAA9AAAATwAAAFAAAABPAAAATwAAAFAAAABQAAAAPQAAAFAAAABQAAAAEwAAAFAAAABs7savTwAAAHwAAABFAAAAUAAAAD4AAAARAAAAEwAAAE8AAABPAAAATwAAAE8AAABAAAAAPgAAAFAAAABQAAAATQAAAFAAAABQAAAAUAAAAFAAAABBAAAAUAAAAE0AAABQAAAAUAAAAFAAAABQAAAATQAAAE8AAAA+AAAAUAAAAE8AAABNAAAAUAAAAFAAAABQAAAATQAAAE8AAABzAAAASQAAAJyappFOAAAAAqKHHUIAAABPAAAATwAAAAIAAABPAAAATwAAAE8AAABOAAAASQAAAE4AAABGAAAAUAAAAE8AAABPAAAATwAAAE8AAABQAAAAUAAAAE8AAABQAAAATgAAAFAAAABNAAAATgAAAEQAAABOAAAASQAAAE8AAABQAAAATwAAAE8AAABPAAAAUAAAAFAAAABJAAAATwAAAE8AAABQAAAAiwAAAPvwnH1KAAAATwAAAHMAAABJAAAAhdH/uEoAAACVJNmyTwAAAIwAAAB9AAAAPwAAANMgVYRCAAAATwAAAE8AAAAAAAAATwAAAE8AAABPAAAAEwAAAD8AAABAAAAAPQAAAE8AAABPAAAATwAAAFAAAABPAAAATwAAAEAAAABPAAAATwAAABMAAABPAAAA95Bq+U8AAAB+AAAARQAAAFAAAABBAAAAEQAAABMAAABQAAAATwAAAFAAAABQAAAAQAAAAEEAAABPAAAATwAAAE0AAABQAAAAUAAAAE8AAABPAAAAQQAAAE8AAABNAAAATwAAAE8AAABQAAAAUAAAAE0AAABPAAAAPgAAAFAAAABPAAAATQAAAE8AAABQAAAATwAAAE0AAABPAAAAcwAAAEkAAAC+rVoYTgAAACS1O6RCAAAAUAAAAE8AAAABAAAAUAAAAFAAAABPAAAATgAAAEkAAABOAAAARgAAAFAAAABPAAAAUAAAAE8AAABPAAAATwAAAFAAAABQAAAAUAAAAE4AAABQAAAATQAAAE4AAABEAAAATgAAAEkAAABQAAAAUAAAAFAAAABQAAAATwAAAE8AAABQAAAASQAAAFAAAABPAAAAUAAAAIsAAACvxIdgSgAAAE8AAABzAAAASQAAAE8o64NKAAAAoCCBJ08AAACMAAAAfwAAAEIAAAAC+JS2QgAAAE8AAABPAAAAAAAAAE8AAABPAAAAUAAAABMAAABCAAAAQwAAAD0AAABQAAAAUAAAAE8AAABQAAAATwAAAFAAAABDAAAAUAAAAFAAAAATAAAATwAAAOVvbBtPAAAAgAAAAEUAAABQAAAARAAAABEAAAATAAAATwAAAE8AAABPAAAATwAAAEAAAABEAAAATwAAAE8AAABNAAAATwAAAFAAAABPAAAATwAAAEEAAABPAAAATQAAAE8AAABPAAAATwAAAFAAAABNAAAAUAAAAHMAAABJAAAAzJgjwU4AAAAyoARNQgAAAFAAAABQAAAAAgAAAE8AAABPAAAATwAAAE4AAABJAAAATgAAAEMAAABPAAAAUAAAAFAAAABOAAAATwAAAE4AAAAFjXpMTwAAAE8AAABNAAAAgQAAAEQAAABOAAAASQAAAFAAAABQAAAATwAAAFAAAABQAAAATwAAAE8AAABJAAAAUAAAAFAAAABQAAAAiwAAADyIlktKAAAATwAAAHMAAABJAAAAB+2YbkoAAADI+KECTwAAAIwAAACCAAAARQAAAONfZN5CAAAAUAAAAFAAAAAAAAAATwAAAE8AAABQAAAAEwAAAEUAAABGAAAAPQAAAFAAAABQAAAATwAAAE8AAABQAAAAUAAAAEYAAABQAAAAUAAAABMAAABQAAAArsBTqFAAAACDAAAARQAAAE8AAABHAAAAEQAAABMAAABQAAAAUAAAAFAAAABPAAAAQAAAAEcAAABPAAAATwAAAE0AAABPAAAAUAAAAFAAAABQAAAAQQAAAFAAAABNAAAATwAAAE8AAABPAAAAUAAAAE0AAABQAAAAPgAAAE8AAABQAAAATQAAAFAAAABPAAAATwAAAE0AAABPAAAAcwAAAEkAAAD91xbTTgAAAGPf915CAAAAUAAAAFAAAAAAAAAAUAAAAFAAAABQAAAATgAAAEkAAABOAAAARgAAAFAAAABQAAAAUAAAAFAAAABQAAAAUAAAAFAAAABQAAAATwAAAE4AAABPAAAATQAAAE4AAABEAAAATgAAAEkAAABQAAAATwAAAE8AAABQAAAAUAAAAE8AAABPAAAASQAAAFAAAABQAAAATwAAAIsAAADhL0KjSgAAAE8AAABzAAAASQAAAOz1TwZKAAAAan/fVU8AAABzAAAASQAAALZOjUVKAAAAwE6biU8AAAA9AAAATwAAAFAAAABQAAAATwAAAFAAAABQAAAABgAAAFAAAABQAAAAAwAAAFAAAABQ0j4lUAAAAIQAAACMAAAAhQAAAFEAAADyKuc7PQAAAE8AAABPAAAATwAAAE8AAABPAAAATwAAAFEAAABQAAAAUAAAAAQAAABQAAAAC+DRC1AAAACGAAAAiwAAAEtWAABIAAAARAAAAEgAAAARAAAATwAAAE8AAABQAAAAUAAAAFAAAABQAAAATwAAABEAAABPAAAATwAAAFAAAABzAAAASQAAAL6IXRJKAAAAuBTLvE8AAABIAAAAUAAAAE8AAAABAAAAUAAAAFAAAABQAAAATwAAAAwAAABQAAAADgAAAFAAAABPAAAAUQAAAFAAAABJAAAAUAAAAFEAAACHAAAACgAAAFAAAABQAAAAUAAAAE8AAABSAAAAUAAAAFAAAABQAAAAUAAAAF5uZkkrAAAACQAAAAcAAAAIAAAAAQAAAFIAAACLAAAAgy0AAEgAAABCAAAAUAAAAE8AAAABAAAAUAAAAE8AAABQAAAAEQAAAEgAAAARAAAAcwAAAEkAAADZFbHdSgAAAJ2Hd/FPAAAAjAAAAAcAAABRAAAAAAAAAG8AAACLAAAAg5MAAEgAAABEAAAASAAAABEAAABQAAAATwAAAE8AAABPAAAATwAAAE8AAABQAAAAEQAAAE8AAABPAAAAUAAAAHMAAABJAAAAfcYOgEoAAAD51hlPTwAAAIwAAACIAAAAUQAAACtvuXhwAAAAdQAAAA0AAAAFAAAABag0kUgAAAA0EAAARAAAAEgAAAARAAAAUAAAAE8AAABPAAAAUAAAAFAAAABQAAAATwAAABEAAABQAAAAUAAAAE8AAABzAAAASQAAAGj1QJxKAAAADqjnMk8AAACMAAAADQAAAFEAAADK79FhPQAAAE8AAABPAAAAUAAAAFAAAABPAAAAUAAAAFEAAABPAAAATwAAAAUAAABPAAAAa1YVu08AAACJAAAAiwAAAK1ZAABIAAAARAAAAEgAAAARAAAATwAAAFAAAABPAAAAUAAAAFAAAABQAAAAUAAAABEAAABPAAAAUAAAAE8AAABzAAAASQAAAFpe+DFKAAAAHD8wnU8AAABEAAAACQAAAAQAAABQAAAATwAAAFAAAABQAAAAUAAAAFAAAABPAAAAUQAAAFAAAABQAAAATwAAAD0AAABQAAAATwAAAFAAAABPAAAATwAAAE8AAABRAAAATwAAAE8AAAAEAAAATwAAAHqIwutQAAAAigAAAIsAAAA0PgAASAAAAEIAAABPAAAATwAAAAEAAABQAAAATwAAAE8AAAARAAAASAAAABEAAABzAAAASQAAAOOdDmxKAAAAk/8ZY08AAACLAAAATVkAAEgAAABCAAAATwAAAE8AAAACAAAAUAAAAFAAAABQAAAAEQAAAEgAAAARAAAAcwAAAEkAAACq7oSKSgAAAMyuo0RPAAAAeAAAAAcAAACLAAAAZI7KjlEAAACMAAAAbQsAQQgAAAAHAAAAUQAAAIsAAADgAwAASAAAAEIAAABPAAAATwAAAAAAAABPAAAAUAAAAFAAAAARAAAASAAAABEAAABzAAAASQAAABqTPZVKAAAAXArrOU8AAAB2AAAAUQAAAIgAAAArb7l4UgAAAAMAAABRAAAAKwAAAAoAAAADAAAAUgAAAAEAAAAFAAAAiwAAABkeAABIAAAAQgAAAE8AAABPAAAAAQAAAE8AAABPAAAATwAAABEAAABIAAAAEQAAAHMAAABJAAAAINtCZkoAAABWwuVoTwAAAIsAAAB5BwAASAAAAEIAAABPAAAATwAAAAIAAABPAAAATwAAAE8AAAARAAAASAAAABEAAABzAAAASQAAAITwhxlKAAAA8qygtU8AAACPAAAAAgAAAI4AAADTN+bxUQAAAI0AAAAqAAAABgAAAFEAAAABAAAAAAAAAIsAAADJLwAASAAAAEIAAABQAAAAUAAAAAEAAABPAAAAUAAAAFAAAAARAAAASAAAABEAAABzAAAASQAAAJsBwCNKAAAA25toq08AAACMAAAAjgAAAFEAAABQ2HCORAAAAFEAAAAFAAAATwAAAE8AAABQAAAATwAAAFAAAABQAAAATwAAAFIAAABPAAAATwAAAE8AAAA9AAAATwAAAE8AAABPAAAATwAAAE8AAABQAAAAUgAAAFAAAABQAAAABQAAAE8AAABtUrW4TwAAAI8AAACLAAAAd6EAAEgAAABEAAAASAAAABEAAABPAAAATwAAAE8AAABPAAAATwAAAFAAAABQAAAAEQAAAE8AAABPAAAATwAAAHMAAABJAAAAoEBN10oAAADWXNv3TwAAAEQAAAANAAAABQAAAE8AAABQAAAAUAAAAE8AAABQAAAAUAAAAFAAAABRAAAATwAAAFAAAABQAAAAKwAAAAwAAAADAAAACwAAAAEAAABRAAAAdQAAAA4AAACQAAAAiTBwckgAAAA3BQAARAAAAEgAAAARAAAATwAAAFAAAABPAAAAUAAAAFAAAABQAAAAUAAAABEAAABQAAAATwAAAE8AAABzAAAASQAAAOBUxDNKAAAAlkhkm08AAACNAAAABAAAAHYAAABRAAAAkQAAACUmxBhSAAAAAwAAAFEAAABHAAAATwAAAFEAAABPAAAATwAAAE8AAABPAAAAkgAAAE8AAABPAAAA5hCNbQUAAABPAAAATwAAAE8AAABSAAAAQAAAAFEAAABPAAAATwAAAE0AAABQAAAAUAAAAFAAAABPAAAAQQAAAE8AAABNAAAAUAAAAE8AAABPAAAATwAAAE0AAABPAAAAPgAAAE8AAABQAAAATQAAAFAAAABPAAAATwAAAE0AAABPAAAAcwAAAEkAAABiVH02TgAAAJ9n31BCAAAATwAAAE8AAAAAAAAAUAAAAE8AAABPAAAATgAAAEkAAABOAAAARgAAAFAAAABPAAAATwAAAE8AAABQAAAATwAAAFAAAABPAAAATwAAAE0AAABQAAAATgAAAE4AAABEAAAATgAAAEkAAABPAAAATwAAAE8AAABPAAAATwAAAE8AAABQAAAASQAAAE8AAABQAAAATwAAAIsAAADKB1GPSgAAAE8AAACLAAAAvGQAAEgAAABCAAAATwAAAE8AAAAAAAAATwAAAE8AAABQAAAAEQAAAEgAAAARAAAAcwAAAEkAAABivE4MSgAAABTh2cJPAAAAiwAAADIpAABIAAAAQgAAAE8AAABQAAAAAgAAAE8AAABPAAAATwAAABEAAABIAAAAEQAAAHMAAABJAAAAVdhMlEoAAAAhxds6TwAAAIsAAADrHgAASAAAAEIAAABQAAAATwAAAAEAAABQAAAATwAAAFAAAAARAAAASAAAABEAAABzAAAASQAAAFjyEC5KAAAAHqsXoU8AAAAqAAAASQAAAEsAAAACAAAASQAAAEoAAAB2AAAATAAAAJMAAABikhaSSQAAAEkAAABMAAAAcQAAAHQAAABaAAAAlAAAAG7L1S1ZAAAAlQAAAAT49HgvAAAAVgAAAIkoAAACAAAADAAAAAAAAAAAAAAAewAAABQAAAAVAAAAlgAAAOxI7GY9AAAAWgAAAFoAAABaAAAAWgAAAFkAAABaAAAAFQAAAFoAAABaAAAAFgAAAFkAAADJk0s5WQAAAJcAAACMAAAADQAAABcAAADK79FhPQAAAFoAAABaAAAAWgAAAFoAAABZAAAAWgAAABcAAABZAAAAWgAAABgAAABZAAAAvX70f1oAAACYAAAAjAAAAJkAAAAZAAAAXLdgCz8AAABZAAAAWQAAAFkAAABZAAAAWQAAAFkAAAAWAAAAGQAAABoAAABaAAAAWgAAAEAAAAAaAAAAWgAAAFkAAABXAAAAWgAAAFkAAABZAAAAWQAAAEEAAABaAAAAVwAAAFoAAABaAAAAWgAAAFoAAABXAAAAWgAAAHMAAABUAAAA+KWrJFgAAAA5ru4eQgAAAFkAAABZAAAAAQAAAFoAAABaAAAAWQAAAFgAAABUAAAAWAAAAEMAAABZAAAAWQAAAFoAAABYAAAAWQAAAFcAAABODqxzWQAAAFkAAABYAAAAmgAAAEQAAABYAAAAVAAAAFkAAABaAAAAWgAAAFkAAABaAAAAWQAAAFkAAABUAAAAWQAAAFkAAABZAAAAiwAAAJGjsrtVAAAATgAAAIwAAACbAAAAGwAAANHrG5s9AAAAWgAAAFoAAABaAAAAWgAAAFoAAABaAAAAGwAAAFkAAABaAAAAGAAAAFoAAAD8CiIIWgAAAJwAAABFAAAAWgAAABwAAAAWAAAAGAAAAFoAAABaAAAAWgAAAFoAAABAAAAAHAAAAFkAAABaAAAAVwAAAFoAAABZAAAAWQAAAFkAAABBAAAAWQAAAFcAAABZAAAAWQAAAFoAAABaAAAAVwAAAFkAAABzAAAAVAAAAK9VjjlYAAAA/apn0UIAAABZAAAAWgAAAAAAAABaAAAAWgAAAFkAAABYAAAAVAAAAFgAAABDAAAAWQAAAFkAAABZAAAAWAAAAFoAAABXAAAAfRAKuVkAAABZAAAAWAAAAJ0AAABEAAAAWAAAAFQAAABZAAAAWQAAAFkAAABZAAAAWQAAAFkAAABZAAAAVAAAAFkAAABZAAAAWQAAAIsAAACDfxiAVQAAAE4AAABzAAAAVAAAAHEDRLRVAAAASMUS5E4AAACMAAAAngAAAB0AAABaAxyfQgAAAFoAAABaAAAAAQAAAFoAAABaAAAAWgAAABgAAAAdAAAAHgAAAD0AAABaAAAAWQAAAFoAAABZAAAAWgAAAFkAAAAeAAAAWgAAAFkAAAAYAAAAWgAAAGRqTiNaAAAAnwAAAEUAAABZAAAAHwAAABYAAAAYAAAAWgAAAFoAAABaAAAAWgAAAEAAAAAfAAAAWQAAAFkAAABXAAAAWQAAAFoAAABaAAAAWgAAAEEAAABaAAAAVwAAAFoAAABaAAAAWgAAAFkAAABXAAAAWgAAAD4AAABaAAAAWgAAAFcAAABZAAAAWgAAAFoAAABXAAAAWgAAAHMAAABUAAAAzMqLs1gAAAB+dbIbQgAAAFkAAABaAAAAAgAAAFkAAABZAAAAWgAAAFgAAABUAAAAWAAAAEYAAABZAAAAWQAAAFoAAABZAAAAWgAAAFoAAABaAAAAWQAAAFoAAABXAAAAWQAAAFgAAABYAAAARAAAAFgAAABUAAAAWQAAAFkAAABaAAAAWQAAAFkAAABaAAAAWQAAAFQAAABZAAAAWQAAAFkAAACLAAAAiRUeaVUAAABOAAAAcwAAAFQAAAD4O/6UVQAAAE/4QCVOAAAAjAAAAKAAAAAgAAAAHLsC80QAAAAgAAAAGAAAAFkAAABaAAAAWgAAAFoAAABaAAAAWgAAAFkAAAAhAAAAWgAAAFoAAABZAAAAPQAAAFoAAABaAAAAWQAAAFoAAABaAAAAWgAAACEAAABaAAAAWQAAABgAAABaAAAAS1D/GVkAAAChAAAARQAAAFoAAAAiAAAAFgAAABgAAABaAAAAWgAAAFoAAABZAAAAQAAAACIAAABaAAAAWgAAAFcAAABaAAAAWQAAAFoAAABZAAAAQQAAAFkAAABXAAAAWgAAAFoAAABaAAAAWgAAAFcAAABZAAAAPgAAAFkAAABZAAAAVwAAAFoAAABaAAAAWQAAAFcAAABZAAAAcwAAAFQAAADNbO0oWAAAAJlKh4BCAAAAWgAAAFoAAAAAAAAAWgAAAFkAAABaAAAAWAAAAFQAAABYAAAARgAAAFoAAABZAAAAWQAAAFoAAABaAAAAWQAAAFkAAABaAAAAWQAAAFcAAABaAAAAWAAAAFgAAABEAAAAWAAAAFQAAABaAAAAWgAAAFkAAABZAAAAWQAAAFoAAABaAAAAVAAAAFkAAABaAAAAWQAAAIsAAADKGKM9VQAAAE4AAABzAAAAVAAAAKX35pxVAAAAEnFYdE4AAACMAAAAogAAACMAAACVWWWWRAAAACMAAAAYAAAAWgAAAFkAAABZAAAAWQAAAFkAAABaAAAAWQAAACQAAABZAAAAWQAAAFoAAAA9AAAAWQAAAFoAAABZAAAAWgAAAFoAAABZAAAAJAAAAFkAAABaAAAAGAAAAFoAAAA6iqxuWgAAAKMAAABFAAAAWgAAACUAAAAWAAAAGAAAAFoAAABZAAAAWQAAAFoAAABAAAAAJQAAAFkAAABaAAAAVwAAAFoAAABaAAAAWgAAAFoAAABBAAAAWgAAAFcAAABaAAAAWgAAAFkAAABaAAAAVwAAAFoAAAA+AAAAWgAAAFkAAABXAAAAWQAAAFoAAABaAAAAVwAAAFkAAABzAAAAVAAAAOi+/IlYAAAAF/LnsUIAAABaAAAAWgAAAAAAAABaAAAAWgAAAFoAAABYAAAAVAAAAFgAAABGAAAAWgAAAFkAAABaAAAAWgAAAFoAAABaAAAAWgAAAFoAAABZAAAAWAAAAFoAAABXAAAAWAAAAEQAAABYAAAAVAAAAFoAAABaAAAAWQAAAFkAAABZAAAAWgAAAFoAAABUAAAAWgAAAFkAAABaAAAAiwAAAAiFKy5VAAAATgAAAHMAAABUAAAAtxFQ5FUAAADbVBN+TgAAAIwAAACkAAAAJgAAAHJgwx1CAAAAWgAAAFoAAAACAAAAWgAAAFoAAABaAAAAGAAAACYAAAAnAAAAPQAAAFkAAABaAAAAWgAAAFkAAABaAAAAWQAAACcAAABaAAAAWgAAABgAAABaAAAADd5Oh1oAAAClAAAARQAAAFoAAAAoAAAAFgAAABgAAABZAAAAWgAAAFoAAABZAAAAQAAAACgAAABZAAAAWgAAAFcAAABZAAAAWgAAAFkAAABaAAAAQQAAAFkAAABXAAAAWgAAAFoAAABaAAAAWQAAAFcAAABaAAAAPgAAAFkAAABaAAAAVwAAAFoAAABZAAAAWgAAAFcAAABZAAAAcwAAAFQAAACAiKSmWAAAADYiTo9CAAAAWQAAAFkAAAACAAAAWQAAAFoAAABaAAAAWAAAAFQAAABYAAAARgAAAFkAAABZAAAAWgAAAFoAAABaAAAAWgAAAFkAAABZAAAAWgAAAFgAAABZAAAAVwAAAFgAAABEAAAAWAAAAFQAAABaAAAAWQAAAFoAAABZAAAAWQAAAFkAAABZAAAAVAAAAFoAAABZAAAAWQAAAIsAAAAoFi90VQAAAE4AAABzAAAAVAAAAIdX2WFVAAAAPaT8oE4AAACMAAAApgAAACkAAAB0QlBoQgAAAFoAAABaAAAAAAAAAFoAAABZAAAAWgAAABgAAAApAAAAKgAAAD0AAABaAAAAWQAAAFoAAABaAAAAWgAAAFoAAAAqAAAAWgAAAFoAAAAYAAAAWQAAAL9kTPVZAAAApwAAAEUAAABaAAAAKwAAABYAAAAYAAAAWQAAAFkAAABZAAAAWQAAAEAAAAArAAAAWgAAAFkAAABXAAAAWQAAAFoAAABaAAAAWQAAAEEAAABaAAAAVwAAAFoAAABZAAAAWgAAAFkAAABXAAAAWQAAAD4AAABZAAAAWQAAAFcAAABaAAAAWQAAAFkAAABXAAAAWQAAAHMAAABUAAAAeE02w1gAAADHog9bQgAAAFkAAABZAAAAAQAAAFoAAABaAAAAWQAAAFgAAABUAAAAWAAAAEYAAABaAAAAWQAAAFoAAABaAAAAWgAAAFkAAABaAAAAWgAAAFoAAABXAAAAWQAAAFgAAABYAAAARAAAAFgAAABUAAAAWQAAAFkAAABaAAAAWgAAAFoAAABZAAAAWQAAAFQAAABaAAAAWgAAAFoAAACLAAAAxtIGAFUAAABOAAAAcwAAAFQAAADDmbl1VQAAAHbpqt1OAAAAjAAAAKgAAAAsAAAAy+MpBkQAAAAsAAAAGAAAAFkAAABZAAAAWQAAAFkAAABaAAAAWgAAAFkAAAAtAAAAWgAAAFkAAABaAAAAPQAAAFkAAABaAAAAWgAAAFoAAABaAAAAWgAAAC0AAABaAAAAWQAAABgAAABaAAAAZrkQlVoAAACpAAAARQAAAFkAAAAuAAAAFgAAABgAAABaAAAAWgAAAFkAAABaAAAAQAAAAC4AAABaAAAAWgAAAFcAAABZAAAAWgAAAFkAAABZAAAAQQAAAFkAAABXAAAAWgAAAFoAAABZAAAAWgAAAFcAAABZAAAAcwAAAFQAAADDOvfVWAAAABKQzG1CAAAAWgAAAFoAAAACAAAAWQAAAFoAAABZAAAAWAAAAFQAAABYAAAAQwAAAFoAAABZAAAAWgAAAFgAAABZAAAAWAAAAGfR+x9aAAAAWQAAAFcAAACqAAAARAAAAFgAAABUAAAAWgAAAFoAAABaAAAAWgAAAFoAAABaAAAAWgAAAFQAAABZAAAAWgAAAFkAAACLAAAA57mNPlUAAABOAAAAcwAAAFQAAACL183DVQAAAFlzKIdOAAAAjAAAAKsAAAAvAAAArUBf/UIAAABaAAAAWgAAAAIAAABaAAAAWgAAAFoAAAAYAAAALwAAADAAAAA9AAAAWQAAAFkAAABZAAAAWgAAAFkAAABaAAAAMAAAAFoAAABaAAAAGAAAAFoAAABafaEvWQAAAKwAAABFAAAAWgAAADEAAAAWAAAAGAAAAFkAAABZAAAAWgAAAFoAAABAAAAAMQAAAFoAAABaAAAAVwAAAFoAAABZAAAAWgAAAFoAAABBAAAAWQAAAFcAAABaAAAAWQAAAFkAAABaAAAAVwAAAFoAAABzAAAAVAAAAB/ShL1YAAAAU/TuZUIAAABaAAAAWQAAAAIAAABaAAAAWQAAAFkAAABYAAAAVAAAAFgAAABDAAAAWgAAAFoAAABaAAAAWAAAAFoAAABYAAAAlyxnAFoAAABaAAAAVwAAAK0AAABEAAAAWAAAAFQAAABaAAAAWQAAAFoAAABaAAAAWgAAAFoAAABaAAAAVAAAAFoAAABZAAAAWQAAAIsAAABQrgsqVQAAAE4AAABzAAAAVAAAAFu4uUxVAAAAPCTvNE4AAACMAAAArgAAADIAAABiLrcURAAAADIAAAAYAAAAWQAAAFoAAABaAAAAWQAAAFoAAABaAAAAWQAAADMAAABZAAAAWgAAAFkAAAA9AAAAWQAAAFoAAABZAAAAWQAAAFkAAABaAAAAMwAAAFoAAABaAAAAGAAAAFkAAADLNGVQWgAAAK8AAABFAAAAWgAAADQAAAAWAAAAGAAAAFkAAABaAAAAWgAAAFoAAABAAAAANAAAAFkAAABZAAAAVwAAAFoAAABaAAAAWQAAAFoAAABBAAAAWgAAAFcAAABaAAAAWQAAAFkAAABZAAAAVwAAAFoAAAA+AAAAWQAAAFoAAABXAAAAWgAAAFkAAABaAAAAVwAAAFkAAABzAAAAVAAAAEM3AdZYAAAAFAQarkIAAABaAAAAWgAAAAIAAABaAAAAWQAAAFoAAABYAAAAVAAAAFgAAABGAAAAWQAAAFoAAABZAAAAWQAAAFoAAABaAAAAWQAAAFoAAABZAAAAVwAAAFoAAABYAAAAWAAAAEQAAABYAAAAVAAAAFkAAABaAAAAWgAAAFoAAABZAAAAWgAAAFkAAABUAAAAWgAAAFoAAABZAAAAiwAAADddFC1VAAAATgAAAHMAAABUAAAAcR1IMVUAAACFfDj7TgAAAIwAAACwAAAANQAAADHVu0RCAAAAWgAAAFoAAAAAAAAAWgAAAFkAAABaAAAAGAAAADUAAAA2AAAAPQAAAFkAAABZAAAAWQAAAFoAAABaAAAAWgAAADYAAABZAAAAWgAAABgAAABaAAAA49RQ2VoAAACxAAAARQAAAFoAAAA3AAAAFgAAABgAAABaAAAAWQAAAFoAAABaAAAAQAAAADcAAABZAAAAWgAAAFcAAABaAAAAWQAAAFkAAABZAAAAQQAAAFoAAABXAAAAWgAAAFoAAABZAAAAWgAAAFcAAABaAAAAcwAAAFQAAAD6kXe0WAAAAEnnTExCAAAAWQAAAFkAAAABAAAAWgAAAFoAAABZAAAAWAAAAFQAAABYAAAAQwAAAFkAAABZAAAAWQAAAFgAAABaAAAAWAAAADeSvDxaAAAAWQAAAFcAAACyAAAARAAAAFgAAABUAAAAWQAAAFoAAABaAAAAWgAAAFoAAABaAAAAWQAAAFQAAABaAAAAWQAAAFkAAACLAAAA1oDUtVUAAABOAAAAcwAAAFQAAACHvd4KVQAAAC9EBfpOAAAAjAAAALMAAAA4AAAAP5w+jUQAAAA4AAAAGAAAAFoAAABZAAAAWQAAAFkAAABaAAAAWgAAAFkAAAA5AAAAWQAAAFkAAABZAAAAPQAAAFkAAABZAAAAWQAAAFoAAABaAAAAWgAAADkAAABZAAAAWQAAABgAAABZAAAAguz8V1oAAAC0AAAARQAAAFoAAAA6AAAAFgAAABgAAABaAAAAWQAAAFoAAABZAAAAQAAAADoAAABaAAAAWgAAAFcAAABaAAAAWgAAAFoAAABZAAAAQQAAAFkAAABXAAAAWgAAAFoAAABaAAAAWQAAAFcAAABaAAAAPgAAAFoAAABaAAAAVwAAAFkAAABaAAAAWgAAAFcAAABZAAAAcwAAAFQAAABhBG/VWAAAAC7iBC1CAAAAWQAAAFoAAAACAAAAWQAAAFkAAABZAAAAWAAAAFQAAABYAAAARgAAAFkAAABaAAAAWgAAAFkAAABaAAAAWQAAAFkAAABZAAAAWQAAAFgAAABZAAAAVwAAAFgAAABEAAAAWAAAAFQAAABZAAAAWQAAAFkAAABaAAAAWQAAAFkAAABZAAAAVAAAAFkAAABZAAAAWgAAAIsAAAAKaWKTVQAAAE4AAABzAAAAVAAAAGG7a3VVAAAApCVFVE4AAACMAAAAtQAAADsAAADbi23XRAAAADsAAAAYAAAAWQAAAFoAAABaAAAAWQAAAFoAAABaAAAAWgAAADwAAABZAAAAWQAAAFoAAAA9AAAAWgAAAFoAAABZAAAAWQAAAFkAAABaAAAAPAAAAFkAAABZAAAAGAAAAFoAAAB2LXKgWgAAALYAAABFAAAAWQAAAD0AAAAWAAAAGAAAAFkAAABaAAAAWQAAAFoAAABAAAAAPQAAAFkAAABaAAAAVwAAAFkAAABZAAAAWgAAAFoAAABBAAAAWgAAAFcAAABaAAAAWQAAAFoAAABaAAAAVwAAAFkAAAA+AAAAWgAAAFkAAABXAAAAWQAAAFoAAABaAAAAVwAAAFoAAABzAAAAVAAAAOBtuB5YAAAAD6GjRkIAAABZAAAAWgAAAAAAAABaAAAAWgAAAFkAAABYAAAAVAAAAFgAAABGAAAAWQAAAFkAAABZAAAAWQAAAFoAAABZAAAAWQAAAFkAAABZAAAAWAAAAFoAAABXAAAAWAAAAEQAAABYAAAAVAAAAFoAAABZAAAAWQAAAFoAAABaAAAAWgAAAFkAAABUAAAAWQAAAFoAAABZAAAAiwAAAHjC52xVAAAATgAAAHMAAABUAAAAY32hfFUAAAADYcPWTgAAAIwAAAC3AAAAPgAAAFDUNlhEAAAAPgAAABgAAABaAAAAWQAAAFoAAABaAAAAWQAAAFkAAABaAAAAPwAAAFoAAABaAAAAWgAAAD0AAABZAAAAWgAAAFoAAABaAAAAWgAAAFoAAAA/AAAAWgAAAFoAAAAYAAAAWQAAAM25u/paAAAAuAAAAEUAAABZAAAAQAAAABYAAAAYAAAAWgAAAFkAAABZAAAAWQAAAEAAAABAAAAAWgAAAFoAAABXAAAAWgAAAFoAAABaAAAAWQAAAEEAAABaAAAAVwAAAFkAAABaAAAAWgAAAFkAAABXAAAAWgAAAHMAAABUAAAAlIlu8lgAAADj3keKQgAAAFoAAABaAAAAAAAAAFoAAABZAAAAWgAAAFgAAABUAAAAWAAAAEMAAABaAAAAWQAAAFoAAABYAAAAWQAAAFcAAAB0M+UHWgAAAFkAAABYAAAAuQAAAEQAAABYAAAAVAAAAFoAAABZAAAAWgAAAFoAAABaAAAAWQAAAFkAAABUAAAAWgAAAFoAAABaAAAAiwAAAPLjX2NVAAAATgAAAHMAAABUAAAA4wGnO1UAAADoimZwTgAAAIwAAAC6AAAAQQAAAEuHPL9CAAAAWgAAAFoAAAABAAAAWgAAAFoAAABZAAAAGAAAAEEAAABCAAAAPQAAAFoAAABaAAAAWgAAAFkAAABZAAAAWQAAAEIAAABZAAAAWgAAABgAAABZAAAAtJsVg1oAAAC7AAAARQAAAFoAAABDAAAAFgAAABgAAABaAAAAWgAAAFoAAABZAAAAQAAAAEMAAABZAAAAWgAAAFcAAABaAAAAWQAAAFkAAABaAAAAQQAAAFkAAABXAAAAWQAAAFoAAABZAAAAWQAAAFcAAABaAAAAPgAAAFkAAABZAAAAVwAAAFoAAABZAAAAWQAAAFcAAABZAAAAcwAAAFQAAABI472kWAAAADjSxOxCAAAAWgAAAFoAAAACAAAAWgAAAFoAAABaAAAAWAAAAFQAAABYAAAARgAAAFkAAABaAAAAWQAAAFoAAABZAAAAWgAAAFoAAABZAAAAWgAAAFcAAABZAAAAWAAAAFgAAABEAAAAWAAAAFQAAABZAAAAWQAAAFkAAABZAAAAWQAAAFkAAABZAAAAVAAAAFoAAABaAAAAWgAAAIsAAADcqMcwVQAAAE4AAABzAAAAVAAAAKojBBNVAAAAj6tJgU4AAACMAAAAvAAAAEQAAAAhFzXiQgAAAFkAAABZAAAAAQAAAFkAAABaAAAAWgAAABgAAABEAAAARQAAAD0AAABaAAAAWQAAAFkAAABZAAAAWgAAAFkAAABFAAAAWgAAAFkAAAAYAAAAWQAAALM8nrpZAAAAvQAAAEUAAABZAAAARgAAABYAAAAYAAAAWQAAAFkAAABaAAAAWQAAAEAAAABGAAAAWgAAAFoAAABXAAAAWgAAAFkAAABaAAAAWgAAAEEAAABaAAAAVwAAAFkAAABaAAAAWQAAAFoAAABXAAAAWgAAAD4AAABaAAAAWgAAAFcAAABaAAAAWgAAAFoAAABXAAAAWgAAAHMAAABUAAAANHKSR1gAAADlHLmvQgAAAFkAAABZAAAAAAAAAFkAAABZAAAAWgAAAFgAAABUAAAAWAAAAEYAAABaAAAAWgAAAFoAAABZAAAAWQAAAFoAAABaAAAAWgAAAFkAAABXAAAAWgAAAFgAAABYAAAARAAAAFgAAABUAAAAWgAAAFkAAABZAAAAWQAAAFkAAABZAAAAWQAAAFQAAABZAAAAWQAAAFoAAACLAAAAmRjjNFUAAABOAAAAcwAAAFQAAADdh9+cVQAAAHpvsFBOAAAAjAAAAL4AAABHAAAAnXfLMkIAAABZAAAAWQAAAAAAAABZAAAAWQAAAFkAAAAYAAAARwAAAEgAAAA9AAAAWQAAAFkAAABaAAAAWQAAAFkAAABaAAAASAAAAFoAAABZAAAAGAAAAFkAAADz/eVQWQAAAL8AAABFAAAAWQAAAEkAAAAWAAAAGAAAAFkAAABaAAAAWgAAAFkAAABAAAAASQAAAFoAAABaAAAAVwAAAFoAAABZAAAAWQAAAFkAAABBAAAAWQAAAFcAAABaAAAAWgAAAFoAAABZAAAAVwAAAFkAAAA+AAAAWQAAAFkAAABXAAAAWgAAAFoAAABZAAAAVwAAAFoAAABzAAAAVAAAAONokKlYAAAA83mJYUIAAABZAAAAWgAAAAIAAABaAAAAWgAAAFkAAABYAAAAVAAAAFgAAABGAAAAWQAAAFkAAABZAAAAWgAAAFoAAABZAAAAWQAAAFkAAABaAAAAWAAAAFkAAABXAAAAWAAAAEQAAABYAAAAVAAAAFoAAABaAAAAWQAAAFkAAABaAAAAWQAAAFoAAABUAAAAWQAAAFkAAABZAAAAiwAAAONzXEhVAAAATgAAAHMAAABUAAAAHhBjYVUAAABDkY83TgAAAIwAAADAAAAASgAAAB/7ukxCAAAAWgAAAFoAAAAAAAAAWgAAAFoAAABaAAAAGAAAAEoAAABLAAAAPQAAAFoAAABaAAAAWQAAAFkAAABaAAAAWQAAAEsAAABaAAAAWgAAABgAAABaAAAAKHXjc1oAAADBAAAARQAAAFoAAABMAAAAFgAAABgAAABaAAAAWgAAAFoAAABZAAAAQAAAAEwAAABaAAAAWgAAAFcAAABaAAAAWgAAAFkAAABZAAAAQQAAAFoAAABXAAAAWQAAAFkAAABaAAAAWQAAAFcAAABaAAAAPgAAAFoAAABZAAAAVwAAAFkAAABZAAAAWQAAAFcAAABaAAAAcwAAAFQAAADM9BKVWAAAAAAXeT1CAAAAWgAAAFoAAAACAAAAWQAAAFoAAABZAAAAWAAAAFQAAABYAAAARgAAAFoAAABaAAAAWgAAAFoAAABaAAAAWQAAAFoAAABaAAAAWgAAAFcAAABaAAAAWAAAAFgAAABEAAAAWAAAAFQAAABZAAAAWgAAAFoAAABZAAAAWgAAAFoAAABaAAAAVAAAAFoAAABaAAAAWgAAAIsAAABV1dQSVQAAAE4AAABzAAAAVAAAAO7jOUhVAAAAFwWYU04AAACMAAAAwgAAAE0AAADYE9SwRAAAAE0AAAAYAAAAWQAAAFkAAABZAAAAWQAAAFkAAABZAAAAWQAAAE4AAABZAAAAWQAAAFkAAAA9AAAAWQAAAFoAAABaAAAAWQAAAFoAAABZAAAATgAAAFkAAABZAAAAGAAAAFkAAAAnwcEYWQAAAMMAAABFAAAAWQAAAE8AAAAWAAAAGAAAAFkAAABaAAAAWQAAAFoAAABAAAAATwAAAFoAAABZAAAAVwAAAFoAAABZAAAAWQAAAFkAAABBAAAAWgAAAFcAAABZAAAAWQAAAFkAAABaAAAAVwAAAFoAAABzAAAAVAAAAGcYjCZYAAAAtW1lvkIAAABaAAAAWgAAAAIAAABaAAAAWgAAAFkAAABYAAAAVAAAAFgAAABDAAAAWQAAAFkAAABaAAAAWAAAAFoAAABYAAAAGfgXfFoAAABZAAAAVwAAAMQAAABEAAAAWAAAAFQAAABaAAAAWgAAAFkAAABaAAAAWgAAAFkAAABZAAAAVAAAAFoAAABZAAAAWgAAAIsAAAADGocRVQAAAE4AAABzAAAAVAAAAEjCT3lVAAAAjSRxO04AAACMAAAAxQAAAFAAAABqcX8/QgAAAFoAAABaAAAAAQAAAFoAAABaAAAAWgAAABgAAABQAAAAUQAAAD0AAABZAAAAWQAAAFoAAABaAAAAWgAAAFkAAABRAAAAWQAAAFkAAAAYAAAAWQAAAFGnujhaAAAAxgAAAEUAAABZAAAAUgAAABYAAAAYAAAAWQAAAFoAAABZAAAAWQAAAEAAAABSAAAAWgAAAFoAAABXAAAAWgAAAFoAAABZAAAAWQAAAEEAAABaAAAAVwAAAFkAAABZAAAAWgAAAFoAAABXAAAAWgAAAHMAAABUAAAAxgH/b1gAAAD1NOaXQgAAAFkAAABaAAAAAgAAAFoAAABZAAAAWQAAAFgAAABUAAAAWAAAAEMAAABZAAAAWQAAAFkAAABYAAAAWgAAAFcAAABigMtEWQAAAFkAAABYAAAAxwAAAEQAAABYAAAAVAAAAFoAAABaAAAAWQAAAFkAAABZAAAAWQAAAFkAAABUAAAAWQAAAFkAAABZAAAAiwAAAMizOWBVAAAATgAAAHMAAABUAAAA7Grn7lUAAACJqwcGTgAAAHMAAABUAAAAd+c39lUAAACHJPCjTgAAAIwAAAANAAAAWwAAAMrv0WE9AAAAWQAAAFkAAABZAAAAWgAAAFoAAABZAAAAWwAAAFkAAABaAAAABwAAAFkAAACjMYyzWQAAAMgAAACLAAAA7wYAAFMAAABEAAAAUwAAABYAAABZAAAAWgAAAFoAAABZAAAAWQAAAFoAAABZAAAAFgAAAFkAAABaAAAAWQAAAHMAAABUAAAAjCCn+FUAAAAj4eW0TgAAAD0AAABZAAAAWQAAAFoAAABaAAAAWgAAAFoAAAAHAAAAWgAAAFkAAABbAAAAWQAAAHdX0PVZAAAAyQAAAIwAAAAFAAAAWwAAAAWoNJFEAAAAWwAAAAcAAABaAAAAWgAAAFoAAABaAAAAWgAAAFkAAABaAAAAEwAAAFoAAABZAAAAWQAAAIsAAADY7QAAUwAAAEIAAABZAAAAWgAAAAIAAABZAAAAWgAAAFkAAAAWAAAAUwAAABYAAABzAAAAVAAAAPBF2fJVAAAAvPqgl04AAABJAAAAWQAAAAoAAADKAAAACQAAAFoAAABZAAAAWQAAAFkAAABbAAAAWQAAAFkAAABZAAAAWgAAAPXc6UE9AAAAWgAAAFoAAABaAAAAWgAAAFkAAABaAAAAWwAAAFoAAABZAAAAAQAAAFoAAAAQo/pXWQAAAMsAAAB1AAAACwAAAMwAAABr+NVxUwAAAAUSAABCAAAAWgAAAFoAAAACAAAAWgAAAFoAAABaAAAAFgAAAFMAAAAWAAAAcwAAAFQAAAC/rThCVQAAAIX+X2JOAAAASgAAAMm+gOfNAAAAWQAAAFoAAABZAAAAAQAAAFkAAABbAAAAWQAAAFkAAABaAAAAWgAAAFkAAAALAAAAWgAAAEsAAABZAAAAWgAAAFkAAABaAAAAAQAAAFoAAABZAAAAWQAAAFsAAABZAAAAWgAAAAwAAABaAAAAWgAAAHUAAAANAAAAzgAAAJLj4vBTAAAAOlkAAEQAAABTAAAAFgAAAFkAAABZAAAAWgAAAFkAAABZAAAAWgAAAFkAAAAWAAAAWgAAAFkAAABZAAAAcwAAAFQAAABq6DFrVQAAAMdC9gBOAAAARgAAAFoAAABaAAAAWQAAAFkAAABZAAAAWgAAAFoAAABaAAAAWQAAAAwAAABaAAAADQAAAFsAAAA9AAAAWQAAAFkAAABaAAAAWQAAAFoAAABaAAAAWwAAAFoAAABaAAAACAAAAFkAAACew9GHWQAAAM8AAACOAAAAZI7Kjg4AAACLAAAAiwAAAJwiAABTAAAAQgAAAFoAAABaAAAAAgAAAFoAAABZAAAAWgAAABYAAABTAAAAFgAAAHMAAABUAAAAq3esflUAAACh8GXcTgAAAIsAAACMuAAAUwAAAEIAAABZAAAAWQAAAAAAAABZAAAAWQAAAFoAAAAWAAAAUwAAABYAAABzAAAAVAAAAJCZY/hVAAAAE0pGLE4AAAB7AAAABQAAAFsAAACFAAAA8irnOz0AAABZAAAAWQAAAFkAAABZAAAAWQAAAFkAAABbAAAAWQAAAFkAAAAGAAAAWQAAAIIh4KpZAAAA0AAAAIsAAABsDAAAUwAAAEIAAABaAAAAWQAAAAEAAABZAAAAWQAAAFoAAAAWAAAAUwAAABYAAABzAAAAVAAAACNcEnVVAAAAT1V2EE4AAAA9AAAAWQAAAFkAAABZAAAAWgAAAFkAAABaAAAAEwAAAFkAAABZAAAABwAAAFkAAACvohhZWQAAANEAAACLAAAAhy0AAFMAAABEAAAAUwAAABYAAABaAAAAWQAAAFoAAABZAAAAWQAAAFoAAABZAAAAFgAAAFoAAABaAAAAWQAAAHMAAABUAAAAFXPSQFUAAAA+UPSoTgAAAHkAAABbAAAAjAAAAG0LAEEPAAAADgAAAFsAAAAQAAAAiAAAACtvuXiLAAAAdIQAAFMAAABEAAAAUwAAABYAAABZAAAAWgAAAFkAAABZAAAAWQAAAFoAAABaAAAAFgAAAFkAAABaAAAAWgAAAHMAAABUAAAAC+hyPlUAAACWf3H/TgAAAJAAAAAAAAAAWwAAABAAAABEAAAABwAAAAIAAABZAAAAWQAAAFoAAABZAAAAWQAAAFoAAABaAAAAXAAAAFoAAABZAAAAWgAAACsAAAARAAAAAAAAAFsAAAABAAAAXAAAAIsAAAD+awAAUwAAAEIAAABaAAAAWQAAAAEAAABaAAAAWgAAAFoAAAAWAAAAUwAAABYAAABzAAAAVAAAAKuNx3JVAAAAuo+QCk4AAACMAAAA0gAAAFsAAAA6dCXPRAAAAFsAAAABAAAAWgAAAFkAAABaAAAAWgAAAFkAAABZAAAAWQAAAAkAAABaAAAAWQAAAFoAAAB1AAAACgAAAA0AAADK79FhUwAAAPCaAABCAAAAWgAAAFkAAAACAAAAWgAAAFkAAABZAAAAFgAAAFMAAAAWAAAAcwAAAFQAAAA+zVD7VQAAAN9mbVJOAAAAPQAAAFoAAABaAAAAWQAAAFoAAABZAAAAWQAAABIAAABaAAAAWgAAAAYAAABaAAAAm214EloAAADTAAAAiwAAAIAvAABTAAAARAAAAFMAAAAWAAAAWgAAAFkAAABZAAAAWgAAAFkAAABaAAAAWQAAABYAAABaAAAAWgAAAFoAAABzAAAAVAAAAP9iEiZVAAAAjglN5E4AAACLAAAAwVcAAFMAAABEAAAAUwAAABYAAABaAAAAWgAAAFoAAABaAAAAWgAAAFoAAABZAAAAFgAAAFoAAABZAAAAWQAAAHMAAABUAAAAhxKw7lUAAACmw7etTgAAAI0AAAAGAAAAfQAAAFsAAAAHAAAAAAAAAFsAAABLAAAAWQAAAFkAAABaAAAAWQAAABEAAABaAAAAWgAAAFoAAAAIAAAAWQAAAFoAAABbAAAAWgAAAFkAAAArAAAAXAAAAA4AAAAPAAAAAQAAAFsAAABEAAAAXAAAAAYAAABaAAAAWgAAAFoAAABaAAAAWQAAAFoAAABaAAAAEgAAAFoAAABZAAAAWgAAAIsAAADEXwAAUwAAAEQAAABTAAAAFgAAAFoAAABaAAAAWgAAAFkAAABaAAAAWgAAAFoAAAAWAAAAWQAAAFkAAABaAAAAcwAAAFQAAABATyHlVQAAAEKdn7VOAAAAiwAAALpGAABTAAAARwAAAFoAAABTAAAAWgAAAFkAAABaAAAAWgAAANQAAABZAAAAWgAAAMM7Ga8WAAAAWQAAAFkAAABaAAAAUwAAAEAAAABTAAAAWQAAAFkAAABXAAAAWQAAAFkAAABaAAAAWgAAAEEAAABaAAAAVwAAAFkAAABZAAAAWQAAAFkAAABXAAAAWQAAAD4AAABaAAAAWgAAAFcAAABZAAAAWQAAAFkAAABXAAAAWQAAAHMAAABUAAAApvYBwFgAAABioWREQgAAAFoAAABaAAAAAAAAAFoAAABaAAAAWQAAAFgAAABUAAAAWAAAAEYAAABZAAAAWQAAAFoAAABaAAAAWQAAAFoAAABZAAAAWgAAAFoAAABXAAAAWgAAAFgAAABYAAAARAAAAFgAAABUAAAAWgAAAFoAAABZAAAAWQAAAFoAAABZAAAAWgAAAFQAAABaAAAAWQAAAFkAAACLAAAA5KHCBlUAAABOAAAAiwAAAAYgAABTAAAARAAAAFMAAAAWAAAAWgAAAFkAAABZAAAAWgAAAFoAAABZAAAAWgAAABYAAABaAAAAWgAAAFkAAABzAAAAVAAAAArAfC1VAAAAVBhyP04AAACLAAAAvh0AAFMAAABEAAAAUwAAABYAAABaAAAAWgAAAFkAAABaAAAAWQAAAFkAAABaAAAAFgAAAFoAAABaAAAAWgAAAHMAAABUAAAAx1zodlUAAACwf0/CTgAAAIsAAACiJAAAUwAAAEcAAABZAAAAUwAAAFkAAABaAAAAWgAAAFkAAADVAAAAWQAAAFoAAACIQ6eVFgAAAFkAAABZAAAAWQAAAFMAAABAAAAAUwAAAFoAAABaAAAAVwAAAFkAAABaAAAAWgAAAFkAAABBAAAAWgAAAFcAAABaAAAAWgAAAFoAAABZAAAAVwAAAFoAAABzAAAAVAAAANVsZFJYAAAAGcIBzkIAAABZAAAAWgAAAAIAAABZAAAAWgAAAFoAAABYAAAAVAAAAFgAAABDAAAAWQAAAFkAAABaAAAAWAAAAFoAAABXAAAAY6Q/NlkAAABZAAAAWAAAANYAAABEAAAAWAAAAFQAAABaAAAAWgAAAFkAAABaAAAAWgAAAFkAAABaAAAAVAAAAFoAAABaAAAAWgAAAIsAAAD8402IVQAAAE4AAACLAAAAzSIAAFMAAABEAAAAUwAAABYAAABaAAAAWgAAAFkAAABaAAAAWgAAAFoAAABaAAAAFgAAAFoAAABaAAAAWgAAAHMAAABUAAAA0DojaFUAAAAC/trDTgAAAIsAAAAFUAAAUwAAAEQAAABTAAAAFgAAAFkAAABaAAAAWgAAAFkAAABaAAAAWQAAAFoAAAAWAAAAWQAAAFkAAABZAAAAcwAAAFQAAAB2zH2dVQAAAFR8UwJOAAAARwAAAFkAAABbAAAAWQAAAFkAAABZAAAAWQAAANcAAABZAAAAWgAAAMcjG7cHAAAAWQAAAFkAAABZAAAAAwAAAEAAAABbAAAAWQAAAFkAAABXAAAAWQAAAFkAAABZAAAAWQAAAEEAAABZAAAAVwAAAFkAAABZAAAAWgAAAFoAAABXAAAAWQAAAD4AAABaAAAAWQAAAFcAAABZAAAAWQAAAFoAAABXAAAAWQAAAHMAAABUAAAAYlKNilgAAABNMNs2QgAAAFoAAABZAAAAAQAAAFkAAABZAAAAWQAAAFgAAABUAAAAWAAAAEYAAABZAAAAWQAAAFkAAABaAAAAWgAAAFkAAABZAAAAWQAAAFoAAABYAAAAWgAAAFcAAABYAAAARAAAAFgAAABUAAAAWgAAAFkAAABZAAAAWgAAAFkAAABaAAAAWgAAAFQAAABZAAAAWQAAAFkAAACLAAAAQ/eiRVUAAABOAAAAiwAAACESAABTAAAAQgAAAFoAAABaAAAAAQAAAFoAAABaAAAAWgAAABYAAABTAAAAFgAAAHMAAABUAAAAdHRewVUAAABdDcSwTgAAAIsAAADujgAAUwAAAEQAAABTAAAAFgAAAFoAAABaAAAAWgAAAFoAAABaAAAAWgAAAFoAAAAWAAAAWgAAAFoAAABZAAAAcwAAAFQAAABD4va0VQAAADbkG1NOAAAAKgAAAFQAAABWAAAAAgAAAFQAAABVAAAAcgAAAIYAAAAEAAAA2AAAAEfmXqsGAAAA2QAAAKyJHcoFAAAA2gAAAMgeKz4DAAAABwAAANsAAAAgFJNgSgAAAEHsLZXcAAAABQAAAAYAAAAFAAAAAAAAAAQAAAAIAAAABQAAAAYAAAAEAAAABAAAAAUAAAAHAAAABAAAAEsAAAAGAAAABAAAAAQAAAAFAAAAAAAAAAUAAAAFAAAABgAAAAgAAAAFAAAABgAAAAcAAAAGAAAABQAAAIwAAAANAAAACAAAAMrv0WFKAAAAQewtldwAAAAFAAAABQAAAAUAAAAHAAAABgAAAAkAAAAEAAAABgAAAAQAAAAGAAAABAAAAAgAAAAFAAAAPQAAAAYAAAAEAAAABgAAAAYAAAAFAAAABQAAAAkAAAAEAAAABgAAAAAAAAAGAAAA1LJrugUAAADdAAAAfgAAAAcAAADeAAAAtU1BHggAAADfAAAAViGPAAkAAAAHAAAACAAAAAoAAADgAAAAhPqOMwsAAAAHAAAACQAAAAIAAAAAAAAACgAAAIwAAAANAAAABwAAAMrv0WFKAAAAs1m/dOEAAAAEAAAABgAAAAYAAAALAAAABQAAAAgAAAAFAAAABgAAAAUAAAAEAAAABAAAAAcAAAAGAAAAPQAAAAUAAAAFAAAABQAAAAYAAAAFAAAABAAAAAgAAAAEAAAABQAAAAAAAAAFAAAARdlgiAYAAADiAAAAjAAAAOMAAAAHAAAAvalQ00sAAAAEAAAABQAAAAYAAAAEAAAAAAAAAAYAAAAGAAAABAAAAAcAAAAGAAAABAAAAAgAAAAGAAAABAAAAIwAAAANAAAABwAAAMrv0WFKAAAAvNRh+eQAAAAGAAAABgAAAAQAAAAIAAAABgAAAAkAAAAGAAAABgAAAAUAAAAFAAAABgAAAAcAAAAGAAAAPQAAAAYAAAAGAAAABgAAAAYAAAAGAAAABAAAAAkAAAAFAAAABgAAAAAAAAAFAAAAnPYkLgQAAADlAAAAjAAAAOYAAAAHAAAAlKwKTEsAAAAFAAAABgAAAAYAAAAEAAAAAAAAAAQAAAAGAAAABQAAAAcAAAAGAAAABgAAAAgAAAAGAAAABgAAAIwAAAANAAAABwAAAMrv0WFKAAAAQewtldwAAAAGAAAABgAAAAYAAAAIAAAABQAAAAkAAAAGAAAABgAAAAYAAAAGAAAABAAAAAcAAAAGAAAAPQAAAAYAAAAEAAAABAAAAAYAAAAEAAAABQAAAAkAAAAGAAAABAAAAAAAAAAGAAAAiUJvSAQAAADnAAAASwAAAAQAAAAFAAAABAAAAAUAAAAAAAAABQAAAAYAAAAFAAAAAQAAAAQAAAAGAAAABwAAAAUAAAAFAAAAjAAAAA0AAAAIAAAAyu/RYUoAAABRRbaKUwAAAAUAAAAFAAAABQAAAAcAAAAFAAAACQAAAAYAAAAGAAAABAAAAAQAAAAEAAAACAAAAAYAAAA9AAAABgAAAAUAAAAEAAAABQAAAAYAAAAGAAAACQAAAAYAAAAGAAAAAAAAAAUAAADkzb3PBgAAAOgAAABMAAAABQAAAAYAAAAGAAAABgAAAAYAAAAAAAAABgAAAAUAAAAFAAAABwAAAAUAAAAGAAAABQAAAAYAAACMAAAADQAAAAgAAADK79FhSgAAAEO6zgLpAAAABgAAAAUAAAAGAAAABwAAAAQAAAAJAAAABgAAAAYAAAAGAAAABQAAAAYAAAAIAAAABQAAAD0AAAAEAAAABAAAAAYAAAAGAAAABQAAAAUAAAAJAAAABQAAAAQAAAAAAAAABQAAAJthfFkGAAAA6gAAADAAAAAHAAAAAQAAAAAAAACNAAAABwAAAH0AAAAHAAAABwAAAAAAAAAHAAAAggAAAAQAAADrAAAA/5QbDgUAAADsAAAAeDeU/wMAAABEAAAAAQAAAAAAAAAEAAAABQAAAAUAAAAEAAAABAAAAAUAAAAFAAAABgAAAAQAAAAFAAAABAAAAIwAAAANAAAABwAAAMrv0WFKAAAAUUW2ilMAAAAEAAAABQAAAAUAAAAGAAAABAAAAAgAAAAFAAAABQAAAAQAAAAEAAAABQAAAAcAAAAFAAAAPQAAAAQAAAAEAAAABAAAAAUAAAAEAAAABQAAAAgAAAAEAAAABQAAAAAAAAAEAAAAUyRExQQAAADtAAAAjAAAAO4AAAAGAAAAAgaSYUQAAAAGAAAAAAAAAAUAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAHAAAABQAAAAUAAAAEAAAAjAAAAA0AAAAGAAAAyu/RYUoAAADDy2+i7wAAAAQAAAAEAAAABAAAAAcAAAAFAAAACAAAAAQAAAAFAAAABAAAAAQAAAAFAAAABgAAAAUAAAA9AAAABQAAAAQAAAAEAAAABAAAAAUAAAAEAAAACAAAAAUAAAAFAAAAAAAAAAUAAACZ45msBAAAAPAAAABMAAAABAAAAAUAAAAFAAAABAAAAAUAAAAAAAAABAAAAAQAAAAFAAAABgAAAAUAAAAEAAAABQAAAAUAAACMAAAADQAAAAcAAADK79FhSgAAAMPLb6LvAAAABQAAAAQAAAAFAAAABgAAAAUAAAAIAAAABQAAAAUAAAAEAAAABQAAAAQAAAAHAAAABQAAAD0AAAAFAAAABAAAAAUAAAAFAAAABQAAAAQAAAAIAAAABAAAAAUAAAAAAAAABQAAAG4tmK4FAAAA8QAAAH4AAAAGAAAA3gAAALVNQR4HAAAA3wAAAFYhjwAIAAAABgAAAAcAAAAJAAAA8gAAALHaABIKAAAABgAAAAgAAAACAAAAAAAAAAkAAACMAAAADQAAAAYAAADK79FhSgAAAMPLb6LvAAAABQAAAAUAAAAEAAAACgAAAAQAAAAHAAAABQAAAAQAAAAEAAAABAAAAAUAAAAGAAAABAAAAD0AAAAEAAAABAAAAAQAAAAFAAAABQAAAAQAAAAHAAAABAAAAAQAAAAAAAAABAAAACZkH4sEAAAA8wAAAIwAAAD0AAAABgAAAKsLet5KAAAAM1u7efUAAAAEAAAABQAAAAQAAAAAAAAABQAAAAcAAAAFAAAABQAAAAUAAAAFAAAABQAAAAYAAAAEAAAASwAAAAQAAAAFAAAABQAAAAUAAAAAAAAABQAAAAUAAAAFAAAABwAAAAUAAAAEAAAABgAAAAQAAAAFAAAAjAAAAA0AAAAHAAAAyu/RYUoAAAD+zNqJ9gAAAAUAAAAEAAAABQAAAAYAAAAFAAAACAAAAAUAAAAFAAAABQAAAAQAAAAEAAAABwAAAAUAAAA9AAAABQAAAAQAAAAEAAAABQAAAAUAAAAFAAAACAAAAAUAAAAFAAAAAAAAAAUAAAA1mOnLBQAAAPcAAACMAAAAkwAAAAYAAABikhaSMQAAAAcAAAABAAAABgAAAAAAAACNAAAABwAAAH0AAAAGAAAABwAAAAAAAAAGAAAAhgAAAAYAAAD4AAAA9/1cWAUAAAD5AAAAAot/hQQAAAD6AAAAtX9AIwMAAAAHAAAA+wAAANFzU91IAAAABAAAAAUAAAACAAAABQAAAAUAAAAFAAAABQAAAAAAAAAFAAAABwAAAAUAAAAFAAAACAAAAAQAAACMAAAA/AAAAAcAAADmlN7DSgAAAP7M2on2AAAABAAAAAQAAAAFAAAAAAAAAAQAAAAJAAAABQAAAAYAAAAGAAAABAAAAAYAAAAHAAAABAAAAEkAAAAGAAAACQAAAP0AAAAIAAAABAAAAAQAAAAGAAAABAAAAAcAAAAEAAAABAAAAAYAAAAFAAAAjl+a24wAAAANAAAACAAAAMrv0WFKAAAA/szaifYAAAAFAAAABAAAAAYAAAAHAAAABAAAAAkAAAAGAAAABgAAAAQAAAAGAAAABAAAAAgAAAAGAAAAPQAAAAQAAAAGAAAABQAAAAUAAAAFAAAABgAAAAkAAAAEAAAABAAAAAAAAAAFAAAAQSz1pAUAAAD+AAAAfgAAAAcAAADeAAAAtU1BHggAAADfAAAAViGPAAkAAAAHAAAACAAAAAoAAAD/AAAAo7PaWgsAAAAHAAAACQAAAAIAAAAAAAAACgAAAIwAAAANAAAABwAAAMrv0WFKAAAAQewtldwAAAAGAAAABQAAAAYAAAALAAAABQAAAAgAAAAFAAAABAAAAAUAAAAFAAAABgAAAAcAAAAFAAAAPQAAAAQAAAAEAAAABQAAAAYAAAAFAAAABQAAAAgAAAAFAAAABAAAAAAAAAAEAAAA2vZlfAUAAAAAAQAAjAAAAAEBAAAHAAAAIeBx1EoAAADDy2+i7wAAAAUAAAAFAAAABgAAAAAAAAAGAAAACAAAAAYAAAAEAAAABQAAAAYAAAAGAAAABwAAAAYAAABLAAAABAAAAAYAAAAFAAAABgAAAAAAAAAFAAAABAAAAAUAAAAIAAAABgAAAAQAAAAHAAAABAAAAAUAAACMAAAADQAAAAgAAADK79FhSgAAAPmJ6pYCAQAABQAAAAYAAAAEAAAABwAAAAQAAAAJAAAABgAAAAQAAAAFAAAABQAAAAQAAAAIAAAABAAAAD0AAAAEAAAABQAAAAQAAAAFAAAABQAAAAYAAAAJAAAABAAAAAUAAAAAAAAABQAAAKE75b0FAAAAAwEAAIwAAAAEAQAABwAAAKWI1fZLAAAABQAAAAUAAAAGAAAABAAAAAAAAAAEAAAABAAAAAUAAAAHAAAABQAAAAQAAAAIAAAABQAAAAQAAACMAAAADQAAAAcAAADK79FhSgAAAEHsLZXcAAAABAAAAAYAAAAFAAAACAAAAAUAAAAJAAAABgAAAAUAAAAGAAAABgAAAAYAAAAHAAAABQAAAD0AAAAGAAAABgAAAAUAAAAEAAAABQAAAAQAAAAJAAAABQAAAAYAAAAAAAAABgAAAKZB1vwFAAAABQEAAEsAAAAGAAAABgAAAAYAAAAGAAAAAAAAAAQAAAAFAAAABgAAAAEAAAAGAAAABgAAAAcAAAAGAAAABAAAAIwAAAANAAAACAAAAMrv0WFKAAAA4vE51AYBAAAGAAAABgAAAAUAAAAHAAAABQAAAAkAAAAEAAAABQAAAAYAAAAGAAAABQAAAAgAAAAGAAAAPQAAAAYAAAAEAAAABAAAAAUAAAAGAAAABAAAAAkAAAAEAAAABQAAAAAAAAAEAAAAhItBnAUAAAAHAQAAjAAAAAgBAAAHAAAAuDwynEsAAAAFAAAABQAAAAQAAAAFAAAAAAAAAAQAAAAEAAAABAAAAAcAAAAGAAAABQAAAAgAAAAFAAAABAAAAIwAAAANAAAABwAAAMrv0WFKAAAAw8tvou8AAAAEAAAABAAAAAUAAAAIAAAABgAAAAkAAAAEAAAABgAAAAQAAAAGAAAABgAAAAcAAAAFAAAAPQAAAAUAAAAFAAAABAAAAAUAAAAEAAAABQAAAAkAAAAGAAAABQAAAAAAAAAFAAAA38nNygYAAAAJAQAAjQAAAAAAAAB9AAAABwAAAAcAAAAAAAAABwAAAA=="), globals, undefined, undefined); +vm.q([], "q", new Closure(undefined, { + m: 0, + U: 19, + G: 0 +}, "z", "z"), {}, null // no arguments object / parameter setup for the root frame +, undefined); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/qualification.json new file mode 100644 index 00000000..83587a23 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/qualification.json @@ -0,0 +1,83 @@ +{ + "id": "exceptions--twelveOptionEncodedStack--149", + "case": "exceptions", + "profile": "twelveOptionEncodedStack", + "seed": 149, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": true, + "encodeBytecode": true, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": true, + "specializedOpcodes": true, + "aliasedOpcodes": true, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": true, + "handlerTable": true, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "emitted-representation-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true, + "concealConstants": true, + "antiInstrumentation": true, + "specializedOpcodes": true, + "macroOpcodes": true, + "aliasedOpcodes": true, + "shuffleOpcodes": true, + "handlerTable": true, + "classObfuscation": true, + "encodeBytecode": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/encoded-class-stack-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/handler-table-boot/encoded.js b/test/vm/jsconfuser-vm/fixtures/handler-table-boot/encoded.js new file mode 100644 index 00000000..40947d9f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/handler-table-boot/encoded.js @@ -0,0 +1,1028 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 13, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + this[op](); + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var VMPrototype = VM.prototype; +VMPrototype[OP.LOAD_CONST] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[OP.LOAD_INT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[OP.LOAD_GLOBAL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[OP.LOAD_UPVALUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); +}; +VMPrototype[OP.LOAD_THIS] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[OP.MOVE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; +}; +VMPrototype[OP.STORE_GLOBAL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; +}; +VMPrototype[OP.STORE_UPVALUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); +}; +VMPrototype[OP.GET_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.SET_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[OP.DELETE_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; +}; +VMPrototype[OP.ADD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; +}; +VMPrototype[OP.SUB] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; +}; +VMPrototype[OP.MUL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; +}; +VMPrototype[OP.DIV] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; +}; +VMPrototype[OP.MOD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; +}; +VMPrototype[OP.EXP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); +}; +VMPrototype[OP.BAND] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; +}; +VMPrototype[OP.BOR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; +}; +VMPrototype[OP.BXOR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; +}; +VMPrototype[OP.SHL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; +}; +VMPrototype[OP.SHR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; +}; +VMPrototype[OP.USHR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; +}; +VMPrototype[OP.LT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; +}; +VMPrototype[OP.GT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; +}; +VMPrototype[OP.LTE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; +}; +VMPrototype[OP.GTE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; +}; +VMPrototype[OP.EQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; +}; +VMPrototype[OP.NEQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; +}; +VMPrototype[OP.LOOSE_EQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; +}; +VMPrototype[OP.LOOSE_NEQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; +}; +VMPrototype[OP.IN] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; +}; +VMPrototype[OP.INSTANCEOF] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_NEG] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_POS] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_NOT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_BITNOT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; +}; +VMPrototype[OP.TYPEOF] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; +}; +VMPrototype[OP.VOID] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + this._operand(); // consumes argument (intended) + // consumes argument (intended) + regs[base + dst] = undefined; +}; +VMPrototype[OP.TYPEOF_SAFE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; +}; +VMPrototype[OP.JUMP] = function () { + var fp = this._f; + var regs = this._regs; + regs[fp + SLOTS.PC] = this._operand(); +}; +VMPrototype[OP.JUMP_IF_FALSE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[OP.JUMP_IF_TRUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[OP.CALL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } +}; +VMPrototype[OP.CALL_METHOD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[OP.NEW] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } +}; +VMPrototype[OP.RETURN] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[OP.THROW] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + throw regs[base + this._operand()]; +}; +VMPrototype[OP.MAKE_CLOSURE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + // unified prototype for new/instanceof + regs[base + dst] = shell; +}; +VMPrototype[OP.BUILD_ARRAY] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; +}; +VMPrototype[OP.BUILD_OBJECT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; +}; +VMPrototype[OP.DEFINE_GETTER] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); +}; +VMPrototype[OP.DEFINE_SETTER] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); +}; +VMPrototype[OP.FOR_IN_SETUP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; +}; +VMPrototype[OP.FOR_IN_NEXT] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } +}; +VMPrototype[OP.TRY_SETUP] = function () { + var fp = this._f; + var regs = this._regs; + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); +}; +VMPrototype[OP.TRY_END] = function () { + var fp = this._f; + var regs = this._regs; + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); +}; +VMPrototype[OP.FINALLY_SETUP] = function () { + var fp = this._f; + var regs = this._regs; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); +}; +VMPrototype[OP.PATCH] = function () { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } +}; +VMPrototype[OP.JUMP_REG] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; +}; +VMPrototype[OP.DEBUGGER] = function () { + debugger; +}; +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/handler-table-boot/qualification.json b/test/vm/jsconfuser-vm/fixtures/handler-table-boot/qualification.json new file mode 100644 index 00000000..6d4749ca --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/handler-table-boot/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "arithmetic--handlerTable--1", + "case": "arithmetic", + "profile": "handlerTable", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": true, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": { + "kind": "value", + "value": 0 + }, + "effectObservation": { + "axis": "uncompared", + "observed": null + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/handler-table-boot/source.js b/test/vm/jsconfuser-vm/fixtures/handler-table-boot/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/handler-table-boot/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/legacy-common/common.js b/test/vm/jsconfuser-vm/fixtures/legacy-common/common.js new file mode 100644 index 00000000..c4c52b84 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/legacy-common/common.js @@ -0,0 +1 @@ +var x = a && foo(); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/encoded.js b/test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/encoded.js new file mode 100644 index 00000000..05963e2b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */3, /* 3 */undefined, /* 4 */1, /* 5 */0, /* 6 */5]; +var BYTECODE = [4, 1, 47, 3, 39, 1, 7, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 42, 6, 2, 1, 5, 9, 3, 4, 6, 0, 3, 3, 0, 45, 3, 4, 2, 0, 5, 4, 0, 5, 3, 5, 0, 5, 5, 0, 5, 4, 5, 0, 5, 6, 0, 27, 6, 3, 5, 40, 6, 90, 13, 5, 0, 3, 11, 6, 4, 5, 5, 4, 6, 0, 5, 4, 0, 11, 6, 3, 5, 5, 3, 6, 39, 55, 45, 4, 0, 5, 3, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/qualification.json b/test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/qualification.json new file mode 100644 index 00000000..e1639599 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/qualification.json @@ -0,0 +1,58 @@ +{ + "id": "loopSharingCffPrefix--baseline--1", + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "case": "loopSharingCffPrefix", + "profile": "baseline", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 30 + }, + "encodedObservation": { + "kind": "value", + "value": 30 + }, + "decodedObservation": { + "kind": "value", + "value": 30 + }, + "effectObservation": { + "axis": "uncompared", + "observed": null + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/source.js b/test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/source.js new file mode 100644 index 00000000..46d04e1b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/loop-sharing-cff-prefix/source.js @@ -0,0 +1,10 @@ +function f(n) { + let s = 1; + let c = 0; + while (s !== 5) { + c = c + n * s; + s = s + 1; + } + return c; +} +window.TEST_OUTPUT = f(3); diff --git a/test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/encoded.js new file mode 100644 index 00000000..9b9b82e7 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/encoded.js @@ -0,0 +1,1317 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */"finished", /* 5 */undefined, /* 6 */0, /* 7 */"Error", /* 8 */"negative", /* 9 */"message", /* 10 */true]; +var BYTECODE = [69, 1, 47, 3, 65, 1, 9, 0, 0, 73, 3, 2, 62, 3, 0, 0, 4, 1, 0, 5, 2, 0, 74, 5, 6, 42, 5, 2, 1, 6, 68, 3, 6, 0, 42, 7, 2, 1, 6, 61, 6, 0, 0, 8, 4, 0, 75, 9, 8, 6, 48, 6, 3, 5, 7, 9, 70, 6, 3, 4, 65, 3, 5, 0, 3, 69, 2, 76, 157, 141, 5, 4, 54, 120, 3, 68, 6, 6, 0, 77, 7, 6, 0, 78, 100, 7, 61, 6, 7, 0, 7, 8, 0, 44, 8, 6, 1, 7, 71, 8, 68, 2, 6, 0, 79, 6, 7, 0, 55, 66, 5, 7, 4, 159, 55, 67, 141, 55, 67, 135, 64, 6, 9, 0, 7, 3, 6, 66, 5, 7, 4, 161, 55, 67, 141, 55, 80, 163, 4, 67, 141, 62, 6, 0, 0, 7, 4, 0, 8, 10, 0, 70, 8, 6, 7, 81, 4, 71, 5, 72, 5, 72, 5, 65, 6, 5, 0, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 62: + { + // LOAD_GLOBAL,LOAD_CONST,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 63: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 64: + { + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 65: + { + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 66: + { + // MOVE,LOAD_INT (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 67: + { + // ALIAS_JUMP_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + regs[fp + SLOTS.PC] = _operands[0]; + break; + } + case 68: + { + // ALIAS_LOAD_CONST_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 69: + { + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 70: + { + // ALIAS_SET_PROP_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + _operands[0]]; + var key = regs[base + _operands[1]]; + var val = regs[base + _operands[2]]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 71: + { + // ALIAS_THROW_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + throw regs[base + _operands[0]]; + break; + } + case 72: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 73: + { + // ALIAS_MOVE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + break; + } + case 74: + { + // ALIAS_UNARY_NEG_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = -regs[base + _operands[1]]; + break; + } + case 75: + { + // ALIAS_GET_PROP_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + // dst = regs[obj][regs[key]] + var dst = _operands[0]; + var obj = regs[base + _operands[1]]; + var key = regs[base + _operands[2]]; + regs[base + dst] = obj[key]; + break; + } + case 76: + { + // ALIAS_FINALLY_SETUP_3_0_2_1 (order: [3,0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[0]]; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: _operands[0], + contReg: _operands[1], + payloadReg: _operands[2], + throwPad: _operands[3] + }); + break; + } + case 77: + { + // ALIAS_LT_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; + break; + } + case 78: + { + // ALIAS_JUMP_IF_FALSE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var src = _operands[0]; + var target = _operands[1]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 79: + { + // ALIAS_ADD_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + break; + } + case 80: + { + // ALIAS_LOAD_INT_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + break; + } + case 81: + { + // ALIAS_JUMP_REG_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + _operands[0]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/qualification.json new file mode 100644 index 00000000..c3a0ca81 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/qualification.json @@ -0,0 +1,73 @@ +{ + "id": "exceptions--macroOpcodesAliasedOpcodes--104", + "case": "exceptions", + "profile": "macroOpcodesAliasedOpcodes", + "seed": 104, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": true, + "specializedOpcodes": false, + "aliasedOpcodes": true, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "macroOpcodes": true, + "aliasedOpcodes": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-alias-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/encoded.js b/test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/encoded.js new file mode 100644 index 00000000..c1ba3185 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/encoded.js @@ -0,0 +1,1323 @@ +var CONSTANTS = [/* 0 */1, /* 1 */2, /* 2 */3, /* 3 */4, /* 4 */5, /* 5 */6, /* 6 */"window", /* 7 */"TEST_OUTPUT", /* 8 */undefined]; +var BYTECODE = [4, 1, 67, 5, 0, 0, 6, 1, 0, 7, 2, 5, 6, 2, 7, 8, 2, 0, 68, 5, 3, 0, 6, 2, 8, 5, 3, 6, 7, 4, 0, 9, 5, 0, 66, 5, 2, 7, 9, 4, 5, 2, 5, 6, 0, 61, 6, 7, 0, 7, 3, 2, 3, 4, 9, 5, 6, 7, 0, 5, 8, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 62: + { + // LOAD_CONST,LOAD_CONST,BUILD_ARRAY,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 63: + { + // LOAD_CONST,LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 64: + { + // LOAD_CONST,BUILD_ARRAY,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 65: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 66: + { + // BUILD_ARRAY,MOVE (macro) + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 67: + { + // LOAD_CONST,LOAD_CONST,BUILD_ARRAY,MOVE,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 68: + { + // LOAD_CONST,BUILD_ARRAY,MOVE,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 69: + { + // BUILD_ARRAY,MOVE,LOAD_CONST,LOAD_CONST,BUILD_ARRAY (macro) + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 70: + { + // MOVE,LOAD_CONST,LOAD_CONST,BUILD_ARRAY,MOVE (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 71: + { + // LOAD_CONST,BUILD_ARRAY,MOVE,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 72: + { + // BUILD_ARRAY,MOVE,LOAD_CONST,LOAD_CONST (macro) + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 73: + { + // MOVE,LOAD_CONST,LOAD_CONST,BUILD_ARRAY (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 74: + { + // BUILD_ARRAY,MOVE,LOAD_CONST (macro) + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 75: + { + // MOVE,LOAD_CONST,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 76: + { + // MOVE,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/qualification.json b/test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/qualification.json new file mode 100644 index 00000000..76acc467 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/qualification.json @@ -0,0 +1,96 @@ +{ + "id": "repeatedArrays--macroOpcodes--16", + "case": "repeatedArrays", + "profile": "macroOpcodes", + "seed": 16, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": true, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ], + [ + 5, + 6 + ] + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ], + [ + 5, + 6 + ] + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ], + [ + 5, + 6 + ] + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/source.js b/test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/source.js new file mode 100644 index 00000000..d9e21348 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-arrays/source.js @@ -0,0 +1 @@ +const a = [1, 2]; const b = [3, 4]; const c = [5, 6]; window.TEST_OUTPUT = [a, b, c]; diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/encoded.js b/test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/encoded.js new file mode 100644 index 00000000..3a6ec553 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/encoded.js @@ -0,0 +1,1030 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 61, 3, 1, 0, 4, 2, 0, 0, 5, 3, 0, 13, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/qualification.json b/test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/qualification.json new file mode 100644 index 00000000..b855b1eb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "arithmetic--macroOpcodes--13", + "case": "arithmetic", + "profile": "macroOpcodes", + "seed": 13, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": true, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": { + "kind": "value", + "value": 0 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/source.js b/test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-boot/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/encoded.js b/test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/encoded.js new file mode 100644 index 00000000..7356c7be --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/encoded.js @@ -0,0 +1,1077 @@ +var CONSTANTS = [/* 0 */0, /* 1 */4, /* 2 */2, /* 3 */1, /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */undefined]; +var BYTECODE = [4, 1, 62, 4, 0, 0, 2, 4, 62, 4, 0, 0, 3, 4, 0, 4, 1, 0, 22, 5, 3, 4, 40, 5, 63, 0, 4, 2, 0, 15, 5, 3, 4, 40, 5, 43, 61, 4, 2, 3, 2, 4, 50, 12, 4, 2, 3, 5, 2, 4, 63, 4, 3, 5, 3, 0, 61, 4, 3, 5, 3, 4, 14, 2, 4, 4, 0, 0, 5, 5, 0, 9, 4, 5, 2, 0, 4, 6, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 6; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ADD,MOVE,JUMP (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // JUMP + regs[fp + SLOTS.PC] = this._operand(); + break; + } + case 62: + { + // LOAD_CONST,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 63: + { + // MOVE,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 64: + { + // ADD,MOVE (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 65: + { + // MOVE,JUMP (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // JUMP + regs[fp + SLOTS.PC] = this._operand(); + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/qualification.json b/test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/qualification.json new file mode 100644 index 00000000..6f8faf97 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "branches--macroOpcodes--13", + "case": "branches", + "profile": "macroOpcodes", + "seed": 13, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": true, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 2 + }, + "encodedObservation": { + "kind": "value", + "value": 2 + }, + "decodedObservation": { + "kind": "value", + "value": 2 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/source.js b/test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/source.js new file mode 100644 index 00000000..a2f57916 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-branches/source.js @@ -0,0 +1 @@ +let value = 0; for (let i = 0; i < 4; i++) { if (i % 2) value += i; else value -= i; } window.TEST_OUTPUT = value; diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/encoded.js b/test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/encoded.js new file mode 100644 index 00000000..e023feaf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/encoded.js @@ -0,0 +1,1088 @@ +var CONSTANTS = [/* 0 */3, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */undefined, /* 4 */1]; +var BYTECODE = [4, 1, 47, 4, 52, 1, 5, 0, 0, 5, 2, 4, 61, 4, 0, 0, 5, 2, 1, 4, 5, 3, 5, 2, 4, 1, 0, 61, 5, 2, 0, 6, 3, 0, 42, 7, 3, 0, 48, 8, 2, 6, 7, 9, 4, 5, 8, 62, 4, 3, 0, 4, 4, 2, 5, 3, 0, 47, 4, 73, 0, 3, 1, 0, 1, 3, 45, 4, 62, 4, 3, 0, 4, 3, 0, 0, 0, 1, 4, 0, 11, 2, 0, 1, 7, 0, 2, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_CONST,CALL (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 62: + { + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/qualification.json b/test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/qualification.json new file mode 100644 index 00000000..231e7f24 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "closures--macroOpcodes--14", + "case": "closures", + "profile": "macroOpcodes", + "seed": 14, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": true, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/source.js b/test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/source.js new file mode 100644 index 00000000..8148a2c5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-closures/source.js @@ -0,0 +1 @@ +function counter(start) { let value = start; return () => ++value; } const next = counter(3); window.TEST_OUTPUT = [next(), next()]; diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/encoded.js new file mode 100644 index 00000000..f47305fc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/encoded.js @@ -0,0 +1,1131 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */"finished", /* 5 */undefined, /* 6 */0, /* 7 */"Error", /* 8 */"negative", /* 9 */"message", /* 10 */true]; +var BYTECODE = [4, 1, 47, 3, 65, 1, 9, 0, 0, 5, 2, 3, 62, 3, 0, 0, 4, 1, 0, 5, 2, 0, 32, 6, 5, 42, 5, 2, 1, 6, 0, 6, 3, 0, 42, 7, 2, 1, 6, 61, 6, 0, 0, 8, 4, 0, 8, 9, 6, 8, 48, 6, 3, 5, 7, 9, 9, 3, 4, 6, 65, 3, 5, 0, 3, 4, 2, 59, 141, 4, 5, 157, 54, 120, 3, 0, 6, 6, 0, 22, 7, 0, 6, 40, 7, 100, 61, 6, 7, 0, 7, 8, 0, 44, 8, 6, 1, 7, 46, 8, 0, 6, 2, 0, 11, 7, 0, 6, 55, 66, 5, 7, 4, 159, 55, 39, 141, 55, 39, 135, 64, 6, 9, 0, 7, 3, 6, 66, 5, 7, 4, 161, 55, 39, 141, 55, 1, 4, 163, 39, 141, 62, 6, 0, 0, 7, 4, 0, 8, 10, 0, 9, 6, 7, 8, 58, 4, 46, 5, 45, 5, 45, 5, 65, 6, 5, 0, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 62: + { + // LOAD_GLOBAL,LOAD_CONST,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 63: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 64: + { + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 65: + { + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 66: + { + // MOVE,LOAD_INT (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/qualification.json new file mode 100644 index 00000000..557a83d0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/qualification.json @@ -0,0 +1,69 @@ +{ + "id": "exceptions--macroOpcodes--15", + "case": "exceptions", + "profile": "macroOpcodes", + "seed": 15, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": true, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-runtime-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/encoded.js new file mode 100644 index 00000000..8b00d7fd --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/encoded.js @@ -0,0 +1,2353 @@ +var CONSTANTS = [/* 0 */"idYHqq8/KKo=", /* 1 */182008480, /* 2 */-182467214, /* 3 */"SdlVM2anIiM2aVvxepl7AXC7SRch67FgGOmhURO5jsB/V33zRG8boZg5b7GbGDqHtQshn7koY6G4Wcbw4HimwzxPujAv2Y5hVvmLENmLhT/liMMQ+Hne4fJY0zCcz+pA9/j8ceTZ4KDmKIOf5AvTgBwYlbFSONegMei/c9rYykD8uXnQ5GgS4PhrLhCruAcArpkNcEzoBiB1uIAwFViDwQZIhNAcKCugKxhycHX4R4BXCF2wQihG4Jh4YNA8SHfARshyUHX4ZWBBmFgwAoi0ABCIjPMsmEsgAKhQUFLIAkCt232wozguoI5pNfCNCCKA5D/1kN3otWEi6IZQPDiQzylY0vD9adgh76jDML+f4AP/iOwQ+qnhIKpY++/De8nAx0jgsbEpn2Be/5iTJheegDsJyJGbeNbgwFsZt43IAUGrySpxothqI++nNRNjiVgBZpmsECa7kycQ64RwMsl/QRw5QtNWV0bjZm92kU85YoF0G1h3e+tvn3gpCrEaWVfBKUeI0z6vkCIImZIRK/l9B2+LRD+eqirhr3kh0bNWBMOXzw5TrPb/YfUZ1rYuC72fDQug9hMZo6EgOcFS68/wQ4LGzFKsufUh5WoE/82L5QbUuuAR2pnSYv/u/VPzt5bCDF7T8RdqkN4bKtu3ixrjjrX5apKCLjeitCc20qB+YcKGSUI+RKplZjf6nm4LmroBCI6EEit2ceIYXnoydbZuXmrKJXZC2lGudjpilkoOR4JjBluSRT5Wojfm230wyrJGPLqxzxBacuZaanASkLYWAq6fLnKl9nRi8K0xNqNa4c/pe8TWICqdvQAm0ZIo/qeDzQbnssItheX/eu3urFumx8nK512g9apiq566M+G31ALujsr1JpWeLh67uFdRy7Z+xMXWVfo+KqOhZyrz+g43haUFI42NGhpnr+sIX3M1tcUlXYHyLXeM4zKvqDJilW8NXP0BFyCrSj9uq3TVN31BzVxHf7NwL0xjZuV5bV8dKIUQC42fBmvf5DNdiK1XNWpDSc90c6PUfy2uvSYVopR3/5qLCRfzvJ8tx+XudGzP20Mvx9HMGF3M5ddklZzYC4GH/hzujNL1y2W2LO+zqlerzOB8+dX1VdU8z6zaF1f8jAxRhL41a6yNHMhngvPcXCXEisUQXK7sdGS0GwisuzRmhAAMRfwXFJ6jLTyoVAzU330BzExURrRSLBBkGfRvjEIcaYRWNCucPmQ/5ElcRcxhxF1EdMxvdJ7kKSy2vVEUkYR3/HKUdBRTvHcsE9TrdH7M9UN0NIGsFVyy5R908JzwC9qHvBw0bKDlJmTRLAi8t0f7zPV8xtXUpeI8o6zZF+P7tAy4law1/Kzp3NJ34fP9XKvEpjVSXansDGeSGwmP9QSGhYINg/w2Z/ajJT+AVB7F+30Sza9UBrt6L3CbL/WZjSwdkbQEM5+fIGNy9UFdes1DxU1bXu9re2HncC11vVgFSINz/wOTQidqvS8tf9W6egzP21skN7GqGB1o9XB1Ap+cAxO3ohIgbY3ldFWazgi7n0fdyv9+weV9pYM+FJOZBwSD1Q72lfAlwq7/2vt3xsvpXvPK5zW1Xtbi6nfKE9SO8ALLleht+PrCZqKrAT7WShrG6H0D0t4mg6P5L76SEgaIjQoakrYpKvXfY2q59lZaXcJLti5Sre8bYtOWChKOvUsGXop2/mdqYSZEukBOf8ZkQm/OflJ+JliiZx5Z9nwKa55XCgq2mRoVbrH5A1KZzkPCWVZU0jt+reEcqpo+A4qUBwv6rW4Smrch1K7N0slHc8KxXkmysikentfq8XfbG46O0TrbkdNpw+LVVvLTyD7qQvTJwXHY6vUm/7v5D8qaFQGLiVQRrqYqI7Pf/XLcxuRBDcmctipbqe8Ye4KWHwmSgR0JAJNc/15jftdhsZhJK8HbejXvbltqJxqpURFkiTgBYZ9OM2unOSNdaWLxLFksz2Y7Gldl62RhW+kXkaw4DIPcFx7zkWh+oXo5A7ED255HcsuxUCip9iF4mbfwC4f9A8qI4zCamShhqPgaV7bLXb/tSOfRi3mE4OIY67PQD+GA6/iwcfQYv6CxK+Pf9kjBwJpZqDG2uB8gmu8qcNmYcgC6udgQ1Gje/wtosNAnuOxBBdiMcGvog6M+OGKwfRhegJMIC5ChKymgvtgAcGj4VkBcyGswSlhV4CN4QRB6iDoAQYgtECP4X2Bj2GIwQEhLwJ5HLfCuWFOgiykqkKMIZIBbGISwKziroTdor/AASJ7DHLizUOfphGHZ6IAQjrsRANeYG/CTaQkg36j/M+bf8UDtyPtRwinfoOUYy+/IeNiA+QmAscoo4WDs/+1Trcg4QZRJAdGseDrv5ZvKMN+IgQFiiYVwM5/5IwWnk9A1SbXBWllxz0g7Y6eT6ANxpgkBgY84bZNcF2jjdWlR0WI5SUFq20d3Eet9IUKpejFHmQkDU4duE0yvVyGk2QpRpvpWR7jLEL8AKWjhAnmfkjIX8oNuD72SOvmgYTjZvbbqS4Xf4Ept8adZDSG1pjcTio/RAvCGgBH2udkm3WrR/8ZLz8YoOrxR09buYuPv0JPPNsmC9RlR9tFqCJ62K1y27toeToT5zVIerrsjEKb1Eih+sQEYipQ+HaqPZ0b6YG5b2mlCkU420rR3EuKRHieyIwpFnl8KWHZPGmOuRjoq1lJOZcJJRnXSab5BIlXmHX4til0GorsNDtKaWOblbWLSQrZ+Qn/ernIF9oNdCyqutiUavo9ieuCW6S3gvdAmNFKH/zhCkcYCMvKt9Obtesvv85vYBrCK/R3g9exi7t/hM6q22cLxTfX1wVrfrtY7GpeWClZ9pQUIlaI+3qOAZ9/yek2RxWxFpU4524xnG+uVHmK1ZAVunUnzZXcN46euBjWO1RZNQPt3V1grYr9GJXmVTn1zjWcHlbNAL2xleeV9bQ7dW0dtS37f3XQE5YOdAi3ftDIDhY9lcwOV", /* 4 */"pwU6vlkSXOZCYlHu", /* 5 */217092359, /* 6 */"LceScwbfjiIhpQ==", /* 7 */-1974029704, /* 8 */undefined, /* 9 */55734110, /* 10 */"EdDzeLvB", /* 11 */-76942206, /* 12 */-1704487393, /* 13 */-455248617, /* 14 */-836625973, /* 15 */"z/nhVsXKvz8KS7nWBXnx4fom", /* 16 */1797199150, /* 17 */"mtg0UFP4QWB3KG6wHQiJgBN4o5E=", /* 18 */322664845, /* 19 */"MHSwzY5cIMSYsw==", /* 20 */-396063444, /* 21 */"Pvi6QFvYLbBeKFDgV3g=", /* 22 */-931850799, /* 23 */"0J2/deb09Rzmi8E3opwa7A==", /* 24 */998484086, /* 25 */"BH8g1UxVZD0yrWsXRftLD32b", /* 26 */-878552777, /* 27 */"OE5/0mZ2auI=", /* 28 */-1962993170, /* 29 */"dnmGwR9J", /* 30 */1404792966, /* 31 */"HAuikBj5h2Ep2d2w+08aww==", /* 32 */127705104, /* 33 */"mvQldY2dIQ2BtAoTl28w4w==", /* 34 */447367891, /* 35 */"pPAfiIQfBIibMJGZ+mE=", /* 36 */823118739, /* 37 */"2krj/vpF09U=", /* 38 */-960790630, /* 39 */"GWqf/Spme9JHvkZCUcZNcpHtIaY=", /* 40 */-1204254544, /* 41 */"e8h5UCYo", /* 42 */501307121, /* 43 */"CpcbCV2BXwk6kRL/", /* 44 */883346755, /* 45 */"4OnQcBnHoUMIP6bQ/GnU4dRo", /* 46 */-298109410, /* 47 */"nr4REviHcgIpjg==", /* 48 */1824798264, /* 49 */"Dx97iX4BQ5llMXen", /* 50 */697863873, /* 51 */"3wnRkac6", /* 52 */2143260908, /* 53 */"LVyL5yJskJx4NIuFDBynjA==", /* 54 */-1257427350, /* 55 */"jqn94ZdmAvKrXzLCsMY=", /* 56 */1397223253, /* 57 */"4YH/ebHwbRudpxErt1AHyQ==", /* 58 */319858154, /* 59 */"753IdcDywh7Pg803", /* 60 */-1033693258, /* 61 */"2tbGe53P", /* 62 */-2140115783, /* 63 */"Ql9pyy11391EraQmapMiDzKD", /* 64 */"ZGpeFlS6VA5emg==", /* 65 */"o2Mm92JxQRk=", /* 66 */"GzOqhgcB", /* 67 */"kHovzZxVFrW0Lh5j/fd1c1kdjoU=", /* 68 */"iMUaTKc8", /* 69 */"HKS0PeeM", /* 70 */"hoIN+fCR", /* 71 */true, /* 72 */"szRoRILNFnyn1Bwk", /* 73 */"bWeKE1W/tgQJhfx9", /* 74 */"tyBiuMZT1cg7QA==", /* 75 */"3GAp+b+RsQjQgA==", /* 76 */"eUDqyDGwslc=", /* 77 */1978368472, /* 78 */-1717666127, /* 79 */-1550641320, /* 80 */"K9+WMBmpCyE=", /* 81 */"OkeDwALJ", /* 82 */-1918052056, /* 83 */"uJD0O9mn2eg=", /* 84 */-1924464846, /* 85 */"XBR1rGokXKRObC783EQ=", /* 86 */-662292647, /* 87 */"5vTpdd7d7ywJtMEb1Iw=", /* 88 */305141433, /* 89 */"b3Z+2ihOiDou1qxpHuI=", /* 90 */466229254, /* 91 */"T926TeTE+0Smz4Gz", /* 92 */-1384357404, /* 93 */"aiqSoR8mtZIp/g==", /* 94 */1301707392, /* 95 */"1rYwGbyBa3mskgEvh6MH17ZS", /* 96 */-800898033, /* 97 */"PYytfBCE7RXjvIss9mea84lM", /* 98 */2135206857, /* 99 */"RT9dScLRFnlC4w==", /* 100 */362938925, /* 101 */"6DoOTovbKUY=", /* 102 */600952082, /* 103 */"/dIobq3jN5a1EiC+pgUeig==", /* 104 */-318803535, /* 105 */"dSx45FV8", /* 106 */-1344651437, /* 107 */"YWK/9jVyjpkdBg==", /* 108 */-1445804953, /* 109 */"90f22+huFfnSYTkplLIaB4aD", /* 110 */-295954746, /* 111 */"eapVNvPbKE4=", /* 112 */-1761074004, /* 113 */"Wyl1oX8pOFFp+w==", /* 114 */-1520551514, /* 115 */"+zP93/5CL8k=", /* 116 */653255880, /* 117 */"yyb27fR9gpY8C+W/7Ar4lsP9820=", /* 118 */"g/8PalyWeDprgn4WYHJf7lyi", /* 119 */"odo0brrleGJ7rnoyFYauAg==", /* 120 */"H9mZsQcpjuAoZ0vz", /* 121 */"PBo/hnMKXZ425q5iPN4rsn1G", /* 122 */"dAWAjQQd", /* 123 */"", /* 124 */"9q3iJdaa6A7C+0AG", /* 125 */"1eHtmhE3josMH4uKBnHymR4h", /* 126 */-1278633996, /* 127 */"B/6vaj2mrT2vkikG", /* 128 */"vUUlzV9bUCd1q0MfPvWQBSKdkzIPt3Mj", /* 129 */"+z3zje8EH/s=", /* 130 */1601287857, /* 131 */-1640906204, /* 132 */"QixgnHjkbXQ=", /* 133 */-2144847203, /* 134 */-2018904150, /* 135 */-1733503351, /* 136 */"PmFO6ZdQaMs=", /* 137 */1339355209, /* 138 */"vDoLTs3FzELZzpNTkafnIg==", /* 139 */-1470529750, /* 140 */"s9ofTrL1KFKCrgsiipeAEjl+iwU=", /* 141 */1549517754, /* 142 */"Op2WBhyDgB9Ys2kl", /* 143 */802907523, /* 144 */"4a7nGt/3+AuZniMJjrF9Gb5i", /* 145 */-324612649, /* 146 */"TmCu2Tiw", /* 147 */1071665107, /* 148 */"F5Wh5Cx/vdM=", /* 149 */1257449937, /* 150 */"6IjJEz6/wyDn6Q==", /* 151 */-360695608, /* 152 */"BeDSaY/Q70iUR/vbhHA7+I1hGKg=", /* 153 */854771063, /* 154 */"vLW9JP3s13P416dM", /* 155 */1492216967, /* 156 */"eYZcGkeuYSY=", /* 157 */1011156644, /* 158 */"nUFn2Yh2JevznyEr", /* 159 */866247226, /* 160 */"ydLnfsrB1Un+PsGqnmdP+7YO", /* 161 */1313178237, /* 162 */"GwSnjCYciuV6ZFzcn0sHROzc", /* 163 */1409993123, /* 164 */"DE5eRk7SXm404jJm", /* 165 */-1266285657, /* 166 */"DEzC3KN3eeySXA4kpbVIHA==", /* 167 */-1979603562, /* 168 */"33s4lKwM", /* 169 */692561461, /* 170 */-1649846698, /* 171 */"Clr5zlm6b1Y=", /* 172 */"aFYB+nBumJkIMg==", /* 173 */"faBSOAZQYNhOQA==", /* 174 */1960287368, /* 175 */"wzD+SN/B+kne8MFY", /* 176 */-526891001, /* 177 */"3PXEnZkMPrQ=", /* 178 */"xh/h8qNmxt0zKg==", /* 179 */923058424, /* 180 */"wF/Q4O5o7RGMiQ==", /* 181 */"ftUiNYlMBduCdyrr", /* 182 */"7Bc9q5M+AqU=", /* 183 */"YjFT2URBePk=", /* 184 */2100425351, /* 185 */"ynfW2OEg8rnsEbqI5gA=", /* 186 */1111237390, /* 187 */"5xc7g5kO", /* 188 */"Iuj8I4S4xjCbmT4A3og3EK6r", /* 189 */"D7ltATKJpHIml58jG7+60idJ", /* 190 */-2120084469, /* 191 */-602755465, /* 192 */"lj1lhZ8SeP+1YzennTJqTQ==", /* 193 */"NePgdg3N9F0cxQnLjF9u6w==", /* 194 */-1265520542, /* 195 */"tVMZx6RC+02J1et6", /* 196 */"sEw39Ytl", /* 197 */-558805857, /* 198 */"NeRHpEw8cYw=", /* 199 */"6MPuz6ZSKuarbRGa0zY4ig==", /* 200 */798324667, /* 201 */"F0it0Dh4z+NPqA==", /* 202 */"lyQItdYdXI0hB1ubRC8Au1Mlbe0=", /* 203 */-1646496362, /* 204 */-1948710713, /* 205 */"HMmAQhrHuLPgXuni32nskZoK", /* 206 */"V/dQExSpnSFlWTTPbHtSx0xLU7E=", /* 207 */"360+ZIfkFGy8mw==", /* 208 */"GRVlvVoNXoV4c1LvIiM=", /* 209 */1366826786, /* 210 */"MGWV/CYLu4cvOw==", /* 211 */"a5BseCiAfwhUsHco", /* 212 */266174696, /* 213 */"6RPB5/FiGe6lVTa6", /* 214 */-2014231230, /* 215 */"GCybpRAUrIz4A+uH6Qz1vA==", /* 216 */"MmTCLFq87wfXjMt87pU=", /* 217 */-146771907, /* 218 */"nlkfwJh4ddA=", /* 219 */2145922790, /* 220 */"dXVH82WfcAOutAUdme0LZQ==", /* 221 */788578394, /* 222 */"hGaI4SRar7Y=", /* 223 */1704115444, /* 224 */"KCLY7iVy", /* 225 */"3FTM1NA9p62qFCV0gQ8=", /* 226 */1093775474, /* 227 */"egA7mJR3", /* 228 */-162290066, /* 229 */"3ZSkCy1/txRclYM9"]; +var BYTECODE = [82, 83, 84, 47, 6, 4190, 1, 87, 0, 0, 85, 42, 5, 6, 1, 7, 47, 8, 7409, 4, 88, 0, 0, 86, 47, 15, 118, 1, 109, 1, 0, 1, 5, 61, 12, 14, 15, 2, 12, 13, 2, 14, 12, 12, 12, 12, 12, 0, 87, 88, 89, 90, 42, 3, 8, 4, 5, 9, 10, 11, 0, 16, 5, 217092358, 62, 12, 17, 14, 12, 13, 16, 3456118059, 6, 14, 12, 42, 16, 2, 1, 17, 0, 17, 7, 2320937596, 42, 18, 2, 1, 17, 91, 116, 9, 3776803505, 10, 202, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 92, 48, 17, 3, 16, 18, 19, 93, 0, 15, 8, 0, 94, 119, 106, 9, 55738001, 104, 10, 2957519308, 105, 11, 4218018039, 107, 12, 2590479903, 47, 100, 10325, 2, 14, 0, 0, 95, 0, 20, 13, 3839745356, 61, 105, 104, 20, 2, 105, 105, 21, 105, 105, 105, 104, 105, 105, 2, 0, 22, 12, 2590479903, 61, 104, 105, 22, 1, 105, 105, 23, 104, 104, 105, 105, 105, 105, 2, 0, 24, 14, 3458344477, 63, 24, 25, 104, 104, 105, 104, 105, 21, 64, 25, 0, 105, 104, 105, 102, 105, 105, 105, 105, 104, 65, 104, 104, 104, 105, 102, 104, 102, 105, 104, 105, 115, 98, 491704917, 103, 3430792244, 66, 103, 98, 105, 105, 227, 106, 103, 105, 67, 15, 104, 103, 103, 102, 105, 104, 3074021491, 105, 105, 105, 105, 105, 68, 104, 105, 103, 105, 105, 104, 98, 104, 104, 98, 105, 105, 105, 0, 105, 1, 99, 2994105604, 77, 0, 26, 16, 1797216172, 61, 104, 104, 26, 1, 104, 104, 23, 104, 104, 104, 104, 104, 105, 0, 69, 104, 23, 21, 104, 105, 104, 27, 104, 64, 27, 1, 104, 104, 104, 102, 104, 105, 104, 105, 104, 65, 105, 105, 105, 105, 102, 104, 102, 105, 104, 104, 115, 98, 2392413167, 103, 3528402095, 66, 103, 98, 104, 104, 326, 106, 103, 105, 67, 17, 105, 103, 103, 102, 104, 104, 3887693234, 104, 105, 105, 105, 104, 68, 105, 104, 103, 105, 105, 105, 98, 104, 105, 98, 104, 105, 105, 2, 104, 1, 99, 421867770, 77, 115, 98, 994206639, 99, 2601872444, 77, 0, 28, 18, 322686461, 68, 104, 104, 28, 105, 105, 105, 29, 104, 105, 23, 105, 105, 105, 0, 105, 61, 105, 105, 29, 0, 105, 105, 23, 105, 104, 104, 104, 105, 105, 0, 69, 105, 23, 21, 104, 105, 105, 30, 105, 64, 30, 2, 105, 105, 104, 102, 104, 104, 105, 105, 104, 65, 105, 105, 104, 105, 102, 104, 102, 105, 105, 104, 115, 98, 972296897, 103, 2831082369, 66, 103, 98, 105, 105, 447, 106, 103, 105, 67, 19, 105, 103, 102, 103, 104, 105, 929652622, 105, 104, 104, 105, 105, 68, 105, 104, 103, 105, 105, 105, 98, 104, 104, 98, 105, 105, 104, 1, 104, 1, 99, 2535368944, 77, 115, 98, 2852417747, 99, 3711585728, 77, 0, 31, 20, 3898913576, 66, 23, 31, 104, 104, 500, 106, 32, 104, 61, 104, 104, 32, 2, 104, 104, 23, 104, 105, 105, 105, 104, 104, 2, 69, 105, 23, 21, 104, 105, 104, 33, 105, 64, 33, 1, 105, 105, 104, 102, 105, 104, 105, 104, 104, 65, 104, 105, 105, 104, 102, 104, 102, 105, 104, 104, 115, 98, 623167676, 103, 3589267068, 66, 103, 98, 104, 104, 561, 106, 103, 105, 67, 21, 104, 103, 103, 102, 105, 105, 1717527315, 105, 104, 104, 104, 105, 68, 104, 105, 103, 104, 104, 104, 98, 104, 105, 98, 104, 104, 104, 0, 104, 1, 99, 3445440619, 77, 115, 98, 589049987, 99, 4164747152, 77, 0, 34, 22, 3363140070, 66, 23, 34, 104, 105, 614, 106, 35, 104, 61, 104, 104, 35, 1, 105, 105, 23, 104, 105, 105, 105, 104, 104, 1, 69, 105, 23, 21, 105, 104, 104, 36, 105, 64, 36, 2, 104, 105, 105, 102, 104, 104, 104, 104, 105, 65, 104, 104, 104, 105, 102, 104, 102, 105, 104, 104, 115, 98, 3587845064, 103, 1151678600, 66, 103, 98, 105, 105, 675, 106, 103, 105, 67, 23, 105, 103, 102, 103, 104, 105, 3177629620, 105, 105, 105, 104, 105, 68, 104, 105, 103, 104, 105, 104, 98, 104, 105, 98, 105, 105, 105, 0, 105, 1, 99, 62991223, 77, 115, 98, 1781063924, 99, 617254213, 77, 0, 37, 24, 998504716, 66, 23, 37, 104, 104, 728, 106, 38, 104, 61, 105, 104, 38, 0, 104, 104, 23, 105, 104, 105, 104, 105, 104, 0, 69, 105, 23, 21, 105, 105, 104, 39, 104, 64, 39, 2, 104, 104, 105, 102, 105, 104, 105, 105, 104, 65, 105, 104, 104, 104, 102, 105, 102, 104, 104, 105, 62, 104, 102, 105, 105, 105, 102, 1701767303, 25, 104, 104, 115, 98, 275730108, 103, 3585662588, 66, 103, 98, 104, 104, 800, 106, 103, 104, 70, 105, 102, 104, 103, 105, 104, 103, 104, 104, 68, 104, 104, 103, 104, 104, 104, 98, 104, 105, 98, 105, 104, 105, 1, 104, 1, 99, 2607923467, 77, 115, 98, 3655599237, 99, 2007739478, 77, 0, 40, 26, 3416419369, 68, 105, 105, 40, 105, 105, 104, 41, 104, 105, 23, 104, 104, 105, 2, 105, 61, 104, 105, 41, 1, 105, 104, 23, 105, 104, 105, 104, 105, 104, 2, 69, 104, 23, 21, 104, 104, 104, 42, 104, 64, 42, 1, 105, 104, 104, 102, 105, 104, 104, 105, 104, 65, 104, 105, 105, 105, 102, 104, 102, 104, 104, 104, 62, 105, 102, 105, 105, 104, 102, 3630924134, 27, 105, 105, 115, 98, 3527697322, 103, 2542924650, 66, 103, 98, 105, 105, 928, 106, 103, 104, 70, 105, 103, 105, 102, 105, 104, 103, 105, 105, 68, 104, 105, 103, 105, 105, 104, 98, 105, 105, 98, 105, 104, 105, 2, 105, 1, 99, 1025813493, 77, 115, 98, 3131494928, 99, 2348983872, 77, 0, 43, 28, 2331972049, 68, 105, 104, 43, 105, 104, 104, 44, 105, 104, 23, 105, 105, 105, 2, 105, 61, 104, 105, 44, 1, 104, 105, 23, 104, 105, 105, 104, 105, 105, 0, 69, 105, 23, 21, 104, 105, 105, 45, 105, 64, 45, 1, 104, 105, 104, 102, 104, 105, 105, 105, 104, 65, 104, 105, 104, 105, 102, 104, 102, 104, 104, 105, 115, 98, 2068555748, 103, 547698340, 66, 103, 98, 104, 105, 1045, 106, 103, 105, 67, 29, 104, 103, 103, 102, 104, 105, 2789269825, 105, 104, 104, 104, 105, 68, 104, 105, 103, 104, 105, 104, 98, 105, 104, 98, 105, 104, 104, 0, 105, 1, 99, 835232531, 77, 115, 98, 2982317167, 99, 2921954620, 77, 0, 46, 30, 1404770119, 68, 105, 105, 46, 105, 105, 105, 47, 104, 105, 23, 104, 105, 105, 2, 105, 61, 105, 105, 47, 0, 105, 105, 23, 105, 104, 105, 104, 104, 104, 0, 69, 104, 23, 21, 104, 105, 105, 48, 105, 64, 48, 2, 105, 105, 105, 102, 105, 104, 105, 105, 104, 65, 105, 104, 105, 105, 102, 105, 102, 105, 105, 104, 62, 105, 102, 104, 105, 104, 102, 67805110, 31, 105, 104, 115, 98, 2550078296, 103, 1556638232, 66, 103, 98, 105, 104, 1177, 106, 103, 104, 70, 105, 102, 105, 103, 104, 104, 103, 104, 105, 68, 104, 104, 103, 104, 105, 105, 98, 105, 105, 98, 105, 105, 105, 1, 105, 1, 99, 2041147335, 77, 115, 98, 4183873142, 99, 2432216419, 77, 0, 49, 32, 127722974, 68, 104, 104, 49, 104, 104, 104, 50, 104, 104, 23, 105, 104, 104, 1, 105, 61, 105, 105, 50, 0, 105, 104, 23, 104, 104, 104, 105, 104, 105, 1, 69, 105, 23, 21, 104, 104, 104, 51, 105, 64, 51, 2, 105, 105, 104, 102, 105, 105, 105, 104, 105, 65, 104, 105, 104, 104, 102, 105, 102, 104, 104, 104, 62, 104, 102, 105, 104, 104, 102, 1539621240, 33, 105, 105, 115, 98, 749447618, 103, 293943426, 66, 103, 98, 104, 104, 1305, 106, 103, 105, 70, 105, 103, 105, 102, 105, 105, 103, 105, 104, 68, 105, 104, 103, 104, 105, 104, 98, 104, 105, 98, 104, 104, 105, 0, 104, 1, 99, 1906640366, 77, 115, 98, 4139499200, 99, 467369777, 77, 0, 52, 34, 447400350, 66, 23, 52, 104, 104, 1354, 106, 53, 104, 61, 104, 104, 53, 1, 105, 104, 23, 104, 104, 104, 105, 105, 104, 1, 69, 104, 23, 21, 104, 105, 105, 54, 104, 64, 54, 0, 105, 104, 104, 102, 105, 104, 104, 105, 105, 65, 105, 105, 105, 104, 102, 104, 102, 104, 105, 104, 62, 105, 102, 105, 105, 105, 102, 1397949184, 35, 105, 105, 115, 98, 1630865865, 103, 301735561, 66, 103, 98, 105, 105, 1426, 106, 103, 105, 70, 105, 103, 105, 102, 105, 104, 103, 105, 105, 68, 105, 105, 103, 104, 104, 105, 98, 104, 105, 98, 105, 105, 105, 2, 104, 1, 99, 1156308185, 77, 115, 98, 3278788439, 99, 3146368611, 77, 0, 55, 36, 823131131, 66, 23, 55, 104, 105, 1475, 106, 56, 105, 61, 105, 105, 56, 2, 105, 105, 23, 104, 105, 105, 104, 104, 104, 0, 69, 105, 23, 21, 104, 104, 104, 57, 104, 64, 57, 0, 104, 104, 105, 102, 104, 105, 104, 104, 105, 65, 105, 104, 104, 105, 102, 105, 102, 104, 104, 105, 115, 98, 4084490580, 103, 1648326164, 66, 103, 98, 104, 104, 1536, 106, 103, 104, 67, 37, 105, 103, 103, 102, 105, 105, 1470458526, 105, 104, 105, 104, 104, 68, 105, 105, 103, 105, 105, 104, 98, 104, 105, 98, 104, 105, 105, 0, 105, 1, 99, 3965046564, 77, 115, 98, 1468988191, 99, 1176653708, 77, 0, 58, 38, 3334164435, 66, 23, 58, 104, 105, 1589, 106, 59, 105, 61, 104, 104, 59, 0, 104, 104, 23, 105, 104, 105, 105, 105, 104, 1, 69, 105, 23, 21, 105, 105, 105, 60, 105, 64, 60, 1, 104, 105, 105, 102, 105, 105, 104, 104, 104, 65, 104, 105, 105, 105, 102, 104, 102, 105, 105, 104, 62, 104, 102, 105, 104, 105, 102, 294754939, 39, 104, 105, 115, 98, 1092225311, 103, 1552292575, 66, 103, 98, 104, 104, 1661, 106, 103, 104, 70, 104, 102, 105, 103, 104, 104, 103, 105, 105, 68, 104, 104, 103, 104, 104, 105, 98, 104, 104, 98, 104, 105, 105, 1, 105, 1, 99, 467366347, 77, 115, 98, 3274129625, 99, 2874568938, 77, 0, 61, 40, 3090711698, 68, 105, 104, 61, 104, 104, 104, 62, 105, 104, 23, 105, 104, 104, 0, 105, 61, 105, 104, 62, 2, 104, 105, 23, 105, 104, 104, 105, 105, 105, 0, 69, 104, 23, 21, 105, 104, 104, 63, 105, 64, 63, 0, 105, 105, 105, 102, 105, 105, 104, 105, 105, 65, 104, 105, 104, 105, 102, 105, 102, 105, 105, 105, 62, 105, 102, 104, 104, 105, 102, 1687184510, 41, 105, 104, 115, 98, 217060910, 103, 106947566, 66, 103, 98, 105, 105, 1789, 106, 103, 105, 70, 105, 102, 105, 103, 104, 105, 103, 105, 104, 68, 105, 105, 103, 105, 105, 104, 98, 104, 105, 98, 104, 104, 104, 1, 104, 1, 99, 3037563034, 77, 115, 98, 17154948, 99, 1290351733, 77, 0, 64, 42, 501317642, 68, 104, 104, 64, 105, 105, 105, 65, 105, 105, 23, 104, 105, 104, 1, 105, 61, 104, 104, 65, 2, 105, 105, 23, 105, 104, 105, 105, 105, 105, 0, 69, 104, 23, 21, 104, 104, 105, 66, 104, 64, 66, 0, 105, 105, 104, 102, 105, 104, 105, 105, 105, 65, 105, 104, 105, 104, 102, 105, 102, 105, 104, 104, 115, 98, 3007964536, 103, 571533880, 66, 103, 98, 105, 105, 1906, 106, 103, 104, 67, 43, 105, 103, 102, 103, 104, 104, 422228593, 104, 105, 104, 105, 104, 68, 104, 104, 103, 104, 104, 104, 98, 105, 104, 98, 105, 105, 104, 0, 105, 1, 99, 914345160, 77, 115, 98, 1511920855, 99, 2417232772, 77, 0, 67, 44, 883336009, 66, 23, 67, 104, 104, 1959, 106, 68, 104, 61, 105, 104, 68, 0, 104, 105, 23, 104, 105, 105, 104, 105, 104, 2, 69, 104, 23, 21, 105, 104, 104, 69, 104, 64, 69, 0, 105, 104, 104, 102, 104, 104, 104, 105, 104, 65, 104, 104, 105, 104, 102, 104, 102, 105, 104, 105, 62, 104, 102, 105, 104, 105, 102, 3092833132, 45, 105, 104, 115, 98, 3321332339, 103, 3037292339, 66, 103, 98, 105, 105, 2031, 106, 103, 105, 70, 104, 102, 105, 103, 104, 105, 103, 104, 104, 68, 104, 105, 103, 105, 104, 105, 98, 105, 105, 98, 104, 105, 105, 2, 105, 1, 99, 3428021919, 77, 115, 98, 323919460, 99, 46566196, 77, 0, 70, 46, 3996852583, 68, 104, 104, 70, 104, 104, 104, 71, 104, 104, 23, 104, 105, 104, 0, 104, 61, 105, 105, 71, 0, 105, 104, 23, 105, 105, 105, 104, 105, 105, 2, 69, 105, 23, 21, 104, 105, 105, 72, 104, 64, 72, 2, 105, 105, 104, 102, 105, 104, 104, 105, 104, 65, 105, 104, 105, 104, 102, 105, 102, 105, 105, 105, 62, 105, 102, 105, 104, 104, 102, 926587806, 47, 104, 105, 115, 98, 3355034508, 103, 3072042316, 66, 103, 98, 105, 104, 2159, 106, 103, 105, 70, 104, 102, 105, 103, 104, 104, 103, 104, 105, 68, 105, 104, 103, 104, 104, 104, 98, 104, 104, 98, 104, 104, 105, 2, 105, 1, 99, 768689404, 77, 115, 98, 2140631805, 99, 810954286, 77, 0, 73, 48, 1824808785, 66, 23, 73, 104, 104, 2208, 106, 74, 104, 61, 105, 104, 74, 2, 104, 104, 23, 105, 104, 104, 104, 105, 104, 1, 69, 105, 23, 21, 105, 104, 104, 75, 104, 64, 75, 1, 105, 104, 105, 102, 104, 104, 105, 105, 104, 65, 105, 104, 104, 104, 102, 105, 102, 104, 104, 104, 62, 104, 102, 104, 104, 105, 102, 2360266379, 49, 105, 105, 115, 98, 3900012169, 103, 2907620937, 66, 103, 98, 105, 105, 2280, 106, 103, 105, 70, 105, 102, 104, 103, 104, 105, 103, 105, 104, 68, 105, 105, 103, 104, 105, 104, 98, 104, 105, 98, 104, 105, 105, 1, 104, 1, 99, 3751504601, 77, 115, 98, 1575668892, 99, 3095942349, 77, 0, 76, 50, 697861518, 68, 105, 105, 76, 104, 104, 105, 77, 105, 105, 23, 104, 105, 104, 1, 104, 61, 105, 104, 77, 1, 104, 104, 23, 104, 104, 104, 105, 104, 105, 0, 69, 104, 23, 21, 105, 105, 105, 78, 105, 64, 78, 1, 105, 105, 105, 102, 104, 104, 104, 104, 105, 65, 104, 105, 104, 104, 102, 105, 102, 105, 105, 104, 62, 104, 102, 105, 105, 105, 102, 4165484730, 51, 105, 104, 115, 98, 4888057, 103, 3316114617, 66, 103, 98, 104, 105, 2408, 106, 103, 105, 70, 105, 103, 105, 102, 105, 105, 103, 104, 104, 68, 104, 105, 103, 105, 104, 105, 98, 104, 105, 98, 104, 104, 104, 1, 104, 1, 99, 726554633, 77, 115, 98, 1876312586, 99, 3357733398, 77, 0, 79, 52, 2143226946, 68, 105, 105, 79, 105, 105, 105, 80, 104, 105, 23, 104, 105, 105, 1, 104, 61, 104, 105, 80, 0, 104, 105, 23, 104, 105, 104, 105, 104, 104, 2, 69, 104, 23, 21, 104, 104, 104, 81, 104, 64, 81, 1, 104, 105, 104, 102, 104, 104, 104, 104, 105, 65, 104, 105, 104, 104, 102, 104, 102, 104, 105, 104, 115, 98, 4081351917, 103, 3096563629, 66, 103, 98, 105, 104, 2525, 106, 103, 105, 67, 53, 104, 103, 103, 102, 104, 104, 240631429, 105, 105, 105, 104, 104, 68, 104, 104, 103, 104, 105, 105, 98, 104, 104, 98, 104, 105, 104, 1, 104, 1, 99, 1629796317, 77, 115, 98, 2329529906, 99, 3145346463, 77, 0, 82, 54, 3037586868, 66, 23, 82, 105, 105, 2578, 106, 83, 105, 61, 105, 104, 83, 1, 104, 104, 23, 104, 104, 105, 104, 105, 105, 1, 69, 104, 23, 21, 104, 104, 104, 84, 104, 64, 84, 0, 104, 104, 104, 102, 104, 104, 105, 104, 104, 65, 104, 105, 104, 104, 102, 104, 102, 105, 105, 104, 115, 98, 1758697448, 103, 395994280, 66, 103, 98, 104, 104, 2639, 106, 103, 104, 67, 55, 104, 103, 102, 103, 104, 104, 2743544463, 104, 104, 105, 104, 104, 68, 105, 104, 103, 105, 104, 104, 98, 105, 104, 98, 105, 104, 104, 2, 104, 1, 99, 260108952, 77, 115, 98, 1649182036, 99, 2705939205, 77, 0, 85, 56, 1397168762, 68, 105, 105, 85, 105, 105, 104, 86, 105, 104, 23, 105, 104, 104, 2, 104, 61, 105, 105, 86, 0, 104, 104, 23, 105, 104, 104, 105, 104, 104, 0, 69, 104, 23, 21, 105, 104, 104, 87, 104, 64, 87, 2, 104, 105, 105, 102, 105, 105, 105, 105, 105, 65, 105, 105, 105, 105, 102, 104, 102, 105, 105, 104, 62, 104, 102, 105, 104, 104, 102, 2071450383, 57, 105, 105, 115, 98, 2273175113, 103, 935655177, 66, 103, 98, 105, 105, 2771, 106, 103, 104, 70, 104, 102, 105, 103, 104, 104, 103, 104, 105, 68, 104, 105, 103, 105, 104, 105, 98, 104, 104, 98, 104, 104, 104, 2, 104, 1, 99, 1399231545, 77, 115, 98, 3564793378, 99, 1562895182, 77, 0, 88, 58, 319830006, 66, 23, 88, 104, 104, 2820, 106, 89, 104, 61, 104, 104, 89, 2, 105, 105, 23, 104, 105, 105, 105, 104, 105, 0, 69, 104, 23, 21, 104, 105, 105, 90, 105, 64, 90, 2, 104, 104, 104, 102, 104, 104, 105, 105, 104, 65, 105, 104, 105, 105, 102, 105, 102, 104, 105, 105, 62, 105, 102, 104, 105, 105, 102, 1300514502, 59, 105, 104, 115, 98, 636063302, 103, 1235194630, 66, 103, 98, 104, 104, 2892, 106, 103, 104, 70, 105, 102, 104, 103, 104, 105, 103, 104, 104, 68, 105, 104, 103, 105, 104, 105, 98, 105, 105, 98, 104, 104, 104, 1, 105, 1, 99, 702295826, 77, 115, 98, 3699926819, 99, 3737700560, 77, 0, 91, 60, 3261291754, 66, 23, 91, 104, 104, 2941, 106, 92, 104, 61, 105, 104, 92, 2, 104, 104, 23, 105, 105, 104, 104, 105, 105, 0, 69, 104, 23, 21, 104, 104, 105, 93, 105, 64, 93, 1, 105, 105, 104, 102, 105, 104, 105, 104, 104, 65, 105, 104, 105, 105, 102, 105, 102, 105, 104, 104, 62, 105, 102, 105, 104, 105, 102, 405400577, 61, 105, 104, 115, 98, 3311941223, 103, 1949501223, 66, 103, 98, 105, 105, 3013, 106, 103, 104, 70, 104, 103, 105, 102, 104, 104, 103, 104, 104, 68, 104, 105, 103, 105, 105, 104, 98, 105, 105, 98, 104, 105, 105, 2, 104, 1, 99, 2452213203, 77, 115, 98, 3968196654, 99, 931008123, 77, 0, 94, 62, 2154854462, 66, 23, 94, 104, 104, 3062, 106, 95, 104, 61, 105, 104, 95, 2, 104, 105, 23, 104, 104, 104, 105, 104, 104, 2, 69, 105, 23, 21, 104, 105, 105, 96, 104, 64, 96, 1, 104, 104, 105, 102, 105, 105, 105, 104, 104, 65, 105, 104, 105, 104, 102, 105, 102, 104, 105, 104, 62, 104, 102, 104, 104, 104, 102, 2817651884, 63, 104, 104, 115, 98, 3718121843, 103, 3615103539, 66, 103, 98, 104, 104, 3134, 106, 103, 105, 70, 105, 103, 104, 102, 105, 104, 103, 104, 104, 68, 104, 104, 103, 104, 104, 104, 98, 104, 105, 98, 104, 104, 104, 0, 104, 1, 99, 942612927, 77, 115, 98, 3918344246, 99, 3432328547, 77, 115, 98, 2774653674, 99, 2677227221, 77, 96, 116, 16, 3705931324, 17, 213, 97, 10818, 66, 21, 97, 105, 105, 3193, 106, 21, 105, 115, 98, 3983350062, 99, 1469053977, 77, 1, 21, 14427, 97, 116, 16, 3776803505, 17, 202, 97, 40176, 68, 105, 105, 97, 105, 105, 104, 21, 105, 105, 21, 105, 105, 105, 1, 105, 115, 98, 1681466877, 99, 3762012140, 77, 55, 115, 4, 4170, 97, 66, 66, 21, 97, 104, 104, 3247, 106, 21, 104, 115, 98, 865614866, 99, 836557117, 77, 55, 1, 97, 50687, 66, 21, 97, 104, 104, 3266, 106, 21, 104, 115, 98, 3324821805, 99, 2119181340, 77, 1, 4, 3634, 55, 1, 97, 18147, 66, 21, 97, 104, 104, 3288, 106, 21, 104, 115, 98, 2586499951, 99, 3394125402, 77, 1, 97, 41255, 71, 104, 21, 97, 104, 97, 105, 65, 104, 1434027182, 64, 97, 2, 104, 104, 104, 102, 104, 105, 104, 104, 104, 65, 105, 104, 104, 105, 102, 104, 102, 104, 104, 105, 62, 105, 102, 104, 104, 104, 102, 1047354552, 66, 105, 104, 115, 98, 1572285811, 103, 2869421587, 66, 103, 98, 105, 104, 3355, 106, 103, 105, 70, 105, 102, 105, 103, 105, 105, 103, 105, 105, 68, 104, 105, 103, 105, 104, 104, 98, 105, 104, 98, 104, 104, 105, 0, 105, 1, 99, 195544512, 77, 1, 97, 11824, 68, 105, 105, 97, 104, 105, 105, 21, 105, 105, 21, 105, 105, 105, 0, 104, 115, 98, 387953428, 99, 1298224707, 77, 1, 97, 0, 68, 105, 104, 97, 105, 105, 104, 21, 105, 105, 21, 105, 104, 104, 0, 105, 115, 98, 1876412200, 99, 3593019927, 77, 1, 97, 46542, 66, 21, 97, 104, 105, 3447, 106, 21, 104, 115, 98, 2576633121, 99, 3421022224, 77, 98, 47, 15, 7409, 4, 88, 0, 0, 99, 1, 97, 28135, 66, 21, 97, 104, 105, 3474, 106, 21, 104, 115, 98, 3198562830, 99, 2799359801, 77, 1, 97, 15604, 71, 104, 21, 97, 104, 97, 104, 67, 104, 4266403136, 64, 97, 2, 104, 104, 105, 102, 104, 105, 104, 105, 104, 65, 105, 105, 104, 105, 102, 104, 102, 105, 104, 105, 115, 98, 3606409121, 103, 144226401, 66, 103, 98, 104, 105, 3530, 106, 103, 104, 67, 68, 105, 103, 102, 103, 104, 105, 610817878, 105, 105, 104, 104, 105, 68, 105, 104, 103, 104, 104, 105, 98, 104, 105, 98, 104, 104, 105, 0, 104, 1, 99, 419395954, 77, 1, 97, 14423, 66, 21, 97, 105, 105, 3576, 106, 21, 105, 115, 98, 3315259179, 99, 2145268246, 77, 1, 97, 15548, 68, 105, 105, 97, 105, 105, 104, 21, 105, 105, 21, 104, 104, 105, 0, 104, 115, 98, 1487281553, 99, 215676864, 77, 100, 101, 1, 97, 57509, 68, 104, 104, 97, 104, 105, 104, 21, 105, 104, 21, 105, 105, 104, 1, 104, 115, 98, 520795600, 99, 1164857343, 77, 1, 21, 8484, 80, 42, 10, 15, 4, 14, 16, 17, 18, 102, 61, 105, 105, 108, 2, 104, 105, 5, 104, 105, 105, 105, 104, 104, 1, 1, 97, 14768, 66, 21, 97, 105, 105, 3674, 106, 21, 105, 115, 98, 1727838378, 99, 3741342869, 77, 1, 97, 37754, 68, 104, 104, 97, 105, 105, 105, 21, 105, 104, 21, 105, 104, 104, 2, 105, 115, 98, 522956655, 99, 1162693210, 77, 71, 105, 0, 108, 105, 6, 105, 69, 105, 687154004, 64, 108, 1, 105, 105, 105, 102, 105, 104, 104, 104, 104, 65, 104, 105, 105, 105, 102, 104, 102, 105, 104, 105, 62, 104, 102, 104, 104, 105, 102, 4074868865, 70, 104, 105, 115, 98, 193401272, 103, 1657208664, 66, 103, 98, 104, 104, 3763, 106, 103, 104, 70, 105, 103, 105, 102, 105, 105, 103, 105, 105, 68, 105, 104, 103, 105, 105, 104, 98, 104, 105, 98, 104, 104, 105, 1, 104, 1, 99, 1178820617, 77, 1, 97, 816, 68, 104, 104, 97, 104, 104, 104, 21, 104, 104, 21, 105, 104, 104, 2, 105, 115, 98, 2048748026, 99, 3932130277, 77, 1, 97, 24943, 68, 104, 104, 97, 105, 104, 105, 21, 105, 105, 21, 105, 104, 104, 0, 105, 115, 98, 2229384190, 99, 3239269865, 77, 1, 21, 38583, 80, 1, 21, 63218, 103, 0, 108, 5, 217092358, 68, 105, 105, 108, 105, 105, 105, 9, 105, 104, 0, 105, 105, 105, 0, 104, 55, 1, 97, 57515, 68, 104, 104, 97, 104, 105, 104, 21, 104, 104, 21, 105, 104, 104, 2, 104, 115, 98, 2866166359, 99, 2603278210, 77, 81, 42, 12, 15, 4, 14, 16, 17, 18, 120, 13, 71, 0, 97, 48894, 66, 21, 97, 105, 104, 3921, 106, 21, 104, 115, 98, 521109860, 99, 1165059155, 77, 139, 21, 27776, 16, 1359064005, 17, 226, 18, 7, 1, 97, 19566, 68, 104, 104, 97, 105, 104, 104, 21, 104, 104, 21, 104, 105, 104, 1, 105, 115, 98, 264863046, 99, 908826737, 77, 61, 105, 104, 9, 1, 105, 105, 5, 105, 105, 105, 104, 105, 104, 1, 1, 4, 3843, 55, 1, 97, 46317, 66, 21, 97, 104, 105, 3992, 106, 21, 105, 115, 98, 1157551370, 99, 16920629, 77, 81, 42, 8, 15, 4, 14, 16, 17, 18, 44, 108, 7, 1, 8, 104, 1, 97, 36797, 71, 104, 21, 97, 105, 97, 105, 72, 105, 2453683631, 64, 97, 2, 105, 104, 104, 102, 105, 105, 104, 104, 104, 65, 105, 104, 105, 105, 102, 105, 102, 105, 105, 104, 115, 98, 1216639819, 103, 3356381291, 66, 103, 98, 105, 105, 4063, 106, 103, 105, 67, 73, 105, 103, 103, 102, 104, 104, 1913154791, 104, 104, 105, 105, 104, 68, 104, 104, 103, 105, 105, 105, 98, 105, 104, 98, 105, 104, 104, 2, 105, 1, 99, 4144340184, 77, 1, 97, 15758, 68, 105, 105, 97, 105, 104, 104, 21, 105, 105, 21, 104, 105, 105, 2, 104, 115, 98, 3658742980, 99, 2321881331, 77, 1, 97, 20976, 68, 105, 105, 97, 105, 105, 105, 21, 105, 105, 21, 105, 104, 105, 1, 104, 115, 98, 4226687981, 99, 1779615196, 77, 105, 106, 120, 6, 12, 2590479903, 97, 12551, 66, 21, 97, 105, 104, 4164, 106, 21, 105, 115, 98, 2153261339, 99, 3298610246, 77, 1, 21, 21687, 0, 108, 8, 0, 107, 42, 98, 100, 2, 98, 99, 0, 101, 74, 3708911840, 108, 109, 119, 81, 75, 3836804626, 84, 12, 2590479903, 82, 76, 3290618080, 83, 77, 1978393884, 47, 78, 10694, 2, 13, 0, 0, 110, 0, 16, 78, 2577274994, 61, 81, 82, 16, 2, 82, 82, 17, 81, 82, 82, 82, 82, 82, 1, 0, 18, 12, 2590479903, 61, 82, 82, 18, 0, 81, 82, 19, 81, 82, 81, 82, 82, 82, 0, 0, 20, 79, 2744346406, 63, 20, 21, 82, 82, 82, 82, 81, 17, 64, 21, 0, 82, 82, 82, 79, 82, 82, 82, 82, 82, 65, 81, 81, 81, 81, 79, 81, 79, 82, 82, 81, 115, 76, 1203644337, 80, 3187279695, 66, 80, 76, 82, 81, 4299, 83, 80, 82, 67, 80, 82, 80, 79, 80, 81, 82, 3142293905, 82, 81, 82, 82, 82, 68, 81, 82, 80, 82, 82, 82, 76, 81, 82, 76, 81, 82, 82, 0, 81, 1, 77, 2232396738, 78, 0, 22, 78, 2577274994, 61, 82, 81, 22, 1, 81, 81, 19, 82, 82, 82, 81, 82, 82, 0, 69, 81, 19, 17, 82, 81, 81, 23, 81, 64, 23, 1, 82, 82, 81, 79, 81, 81, 81, 82, 82, 65, 81, 82, 82, 82, 79, 82, 79, 82, 82, 81, 62, 82, 79, 81, 81, 81, 79, 464653953, 81, 82, 82, 115, 76, 3585729742, 80, 294027936, 66, 80, 76, 82, 82, 4409, 83, 80, 82, 70, 82, 79, 82, 80, 81, 81, 80, 82, 82, 68, 82, 82, 80, 82, 82, 81, 76, 82, 82, 76, 81, 82, 82, 2, 82, 1, 77, 4085717495, 78, 115, 76, 4161598418, 77, 2329742376, 78, 0, 24, 82, 2376893874, 68, 82, 81, 24, 81, 82, 81, 25, 82, 82, 19, 81, 81, 82, 2, 81, 61, 82, 82, 25, 2, 82, 82, 19, 81, 82, 82, 81, 82, 82, 1, 69, 82, 19, 17, 82, 82, 82, 26, 81, 64, 26, 0, 82, 81, 81, 79, 82, 82, 81, 81, 81, 65, 81, 81, 81, 81, 79, 81, 79, 81, 81, 81, 115, 76, 1643732042, 80, 2646997867, 66, 80, 76, 81, 81, 4526, 83, 80, 81, 67, 83, 82, 80, 79, 80, 81, 81, 1439413980, 81, 81, 82, 82, 81, 68, 81, 81, 80, 81, 82, 81, 76, 81, 81, 76, 81, 82, 82, 0, 82, 1, 77, 1142424608, 78, 115, 76, 3162867585, 77, 2745401154, 78, 0, 27, 84, 2370496468, 66, 19, 27, 82, 82, 4579, 83, 28, 82, 61, 81, 81, 28, 2, 82, 82, 19, 81, 81, 82, 82, 82, 82, 1, 69, 81, 19, 17, 81, 81, 81, 29, 81, 64, 29, 0, 82, 81, 81, 79, 81, 82, 82, 82, 81, 65, 81, 82, 81, 82, 79, 81, 79, 81, 82, 81, 115, 76, 2354683211, 80, 3357948393, 66, 80, 76, 82, 81, 4640, 83, 80, 82, 67, 85, 82, 80, 79, 80, 82, 81, 3209592420, 82, 82, 82, 82, 82, 68, 82, 82, 80, 81, 82, 81, 76, 82, 82, 76, 82, 82, 81, 2, 82, 1, 77, 1550227964, 78, 115, 76, 1065116563, 77, 3177740221, 78, 0, 30, 86, 3632671458, 66, 19, 30, 82, 82, 4693, 83, 31, 82, 61, 81, 81, 31, 2, 82, 82, 19, 82, 81, 81, 82, 82, 81, 2, 69, 81, 19, 17, 82, 81, 82, 32, 81, 64, 32, 1, 81, 81, 82, 79, 81, 81, 82, 81, 82, 65, 82, 81, 81, 81, 79, 81, 79, 82, 81, 81, 62, 81, 79, 81, 82, 82, 79, 1303065053, 87, 82, 81, 115, 76, 3503263674, 80, 211560565, 66, 80, 76, 81, 82, 4765, 83, 80, 81, 70, 81, 79, 81, 80, 82, 82, 80, 81, 82, 68, 81, 81, 80, 82, 82, 82, 76, 81, 82, 76, 81, 82, 82, 2, 82, 1, 77, 3358842753, 78, 115, 76, 1047252718, 77, 1509850103, 78, 0, 33, 88, 305161345, 68, 81, 81, 33, 82, 81, 82, 34, 81, 81, 19, 82, 81, 81, 2, 82, 61, 81, 82, 34, 0, 82, 82, 19, 81, 81, 81, 81, 82, 81, 2, 69, 81, 19, 17, 81, 81, 82, 35, 81, 64, 35, 0, 81, 82, 81, 79, 81, 82, 81, 82, 82, 65, 81, 81, 82, 81, 79, 82, 79, 81, 81, 81, 115, 76, 2114104343, 80, 3150924753, 66, 80, 76, 81, 81, 4882, 83, 80, 82, 67, 89, 82, 80, 80, 79, 81, 82, 2788481939, 82, 82, 81, 82, 82, 68, 82, 81, 80, 81, 81, 82, 76, 81, 82, 76, 81, 81, 82, 2, 81, 1, 77, 2459396411, 78, 115, 76, 4010525973, 77, 233780059, 78, 0, 36, 90, 466269907, 66, 19, 36, 81, 81, 4935, 83, 37, 81, 61, 82, 82, 37, 1, 81, 81, 19, 81, 81, 81, 82, 81, 81, 2, 69, 81, 19, 17, 81, 82, 82, 38, 82, 64, 38, 2, 82, 81, 82, 79, 81, 81, 81, 81, 82, 65, 82, 82, 82, 81, 79, 82, 79, 82, 81, 81, 62, 81, 79, 82, 82, 81, 79, 1496902979, 91, 82, 81, 115, 76, 3337894319, 80, 46192137, 66, 80, 76, 81, 81, 5007, 83, 80, 81, 70, 82, 80, 82, 79, 82, 82, 80, 82, 81, 68, 81, 82, 80, 81, 81, 81, 76, 81, 81, 76, 81, 82, 81, 2, 81, 1, 77, 1009847234, 78, 115, 76, 665731814, 77, 413932061, 78, 0, 39, 92, 2910611547, 66, 19, 39, 82, 82, 5056, 83, 40, 82, 61, 82, 81, 40, 2, 82, 81, 19, 81, 82, 82, 81, 82, 82, 1, 69, 82, 19, 17, 82, 81, 81, 41, 82, 64, 41, 0, 81, 82, 82, 79, 81, 82, 82, 81, 82, 65, 81, 82, 81, 81, 79, 81, 79, 81, 82, 82, 115, 76, 3842975267, 80, 551272838, 66, 80, 76, 82, 81, 5117, 83, 80, 81, 67, 93, 81, 80, 79, 80, 81, 82, 4186839735, 81, 82, 81, 81, 81, 68, 82, 81, 80, 82, 81, 82, 76, 82, 81, 76, 81, 81, 82, 0, 81, 1, 77, 836171415, 78, 115, 76, 3518550509, 77, 2912842271, 78, 0, 42, 94, 1301735571, 68, 82, 82, 42, 81, 81, 81, 43, 81, 81, 19, 82, 81, 82, 2, 82, 61, 82, 82, 43, 0, 82, 81, 19, 81, 81, 81, 82, 82, 81, 2, 69, 82, 19, 17, 82, 82, 82, 44, 82, 64, 44, 1, 82, 81, 82, 79, 81, 82, 81, 81, 82, 65, 81, 81, 81, 82, 79, 82, 79, 82, 81, 81, 62, 81, 79, 81, 81, 82, 79, 1579670157, 95, 81, 81, 115, 76, 2420063132, 80, 3423327771, 66, 80, 76, 81, 82, 5249, 83, 80, 81, 70, 82, 79, 81, 80, 81, 81, 80, 81, 81, 68, 81, 81, 80, 82, 82, 81, 76, 81, 82, 76, 81, 82, 82, 0, 81, 1, 77, 1129939309, 78, 115, 76, 3936567921, 77, 2198450920, 78, 0, 45, 96, 3494068141, 66, 19, 45, 82, 82, 5298, 83, 46, 82, 61, 81, 82, 46, 2, 82, 82, 19, 81, 81, 82, 82, 81, 82, 0, 69, 82, 19, 17, 81, 81, 82, 47, 82, 64, 47, 0, 81, 81, 81, 79, 82, 82, 81, 81, 81, 65, 82, 81, 81, 81, 79, 81, 79, 82, 81, 82, 62, 82, 79, 81, 81, 82, 79, 1538902584, 97, 81, 82, 115, 76, 1509266687, 80, 2512532144, 66, 80, 76, 81, 81, 5370, 83, 80, 81, 70, 82, 79, 82, 80, 81, 81, 80, 82, 81, 68, 82, 82, 80, 81, 82, 82, 76, 81, 81, 76, 81, 81, 82, 1, 81, 1, 77, 1331406900, 78, 115, 76, 116286271, 77, 3632040197, 78, 0, 48, 98, 2135202588, 66, 19, 48, 81, 81, 5419, 83, 49, 81, 61, 81, 82, 49, 0, 82, 81, 19, 81, 82, 82, 81, 81, 81, 0, 69, 81, 19, 17, 81, 81, 81, 50, 82, 64, 50, 1, 81, 81, 81, 79, 82, 81, 82, 82, 82, 65, 81, 82, 82, 81, 79, 82, 79, 82, 82, 81, 115, 76, 1523224097, 80, 2526488771, 66, 80, 76, 82, 81, 5480, 83, 80, 82, 67, 99, 82, 80, 80, 79, 81, 82, 3920201313, 81, 81, 82, 81, 81, 68, 82, 82, 80, 82, 81, 81, 76, 81, 82, 76, 82, 81, 81, 0, 81, 1, 77, 2718317435, 78, 115, 76, 3285140574, 77, 13533564, 78, 0, 51, 100, 362943074, 68, 81, 82, 51, 82, 81, 81, 52, 82, 82, 19, 81, 81, 81, 2, 82, 61, 81, 81, 52, 1, 82, 82, 19, 82, 81, 82, 82, 82, 82, 1, 69, 82, 19, 17, 81, 81, 81, 53, 81, 64, 53, 0, 82, 82, 82, 79, 82, 81, 81, 82, 82, 65, 82, 81, 81, 81, 79, 82, 79, 82, 81, 81, 62, 81, 79, 81, 81, 82, 79, 3010608653, 101, 81, 81, 115, 76, 1653709624, 80, 2656973944, 66, 80, 76, 82, 82, 5612, 83, 80, 82, 70, 81, 80, 82, 79, 82, 82, 80, 82, 81, 68, 82, 81, 80, 82, 82, 81, 76, 82, 81, 76, 82, 82, 82, 1, 81, 1, 77, 4278527231, 78, 115, 76, 1055766918, 77, 3759771103, 78, 0, 54, 102, 600939185, 68, 81, 81, 54, 82, 82, 82, 55, 81, 81, 19, 81, 82, 81, 0, 82, 61, 81, 81, 55, 0, 82, 82, 19, 81, 82, 81, 81, 82, 81, 2, 69, 82, 19, 17, 82, 82, 81, 56, 82, 64, 56, 0, 81, 82, 81, 79, 81, 81, 81, 82, 81, 65, 81, 82, 82, 82, 79, 81, 79, 81, 82, 82, 115, 76, 2063332935, 80, 3066597408, 66, 80, 76, 82, 81, 5729, 83, 80, 81, 67, 103, 81, 80, 79, 80, 81, 81, 2801107466, 82, 82, 82, 82, 81, 68, 82, 81, 80, 81, 81, 81, 76, 81, 81, 76, 82, 82, 82, 1, 82, 1, 77, 1694936302, 78, 115, 76, 4088396905, 77, 1983678474, 78, 0, 57, 104, 3976146846, 68, 81, 82, 57, 81, 82, 82, 58, 82, 82, 19, 82, 81, 82, 0, 81, 61, 81, 82, 58, 0, 82, 81, 19, 82, 82, 82, 81, 82, 81, 0, 69, 82, 19, 17, 82, 82, 81, 59, 81, 64, 59, 0, 81, 81, 81, 79, 81, 81, 81, 81, 82, 65, 82, 82, 81, 81, 79, 82, 79, 81, 82, 81, 62, 82, 79, 82, 81, 81, 79, 1061832326, 105, 82, 82, 115, 76, 727228217, 80, 1730493311, 66, 80, 76, 82, 82, 5861, 83, 80, 81, 70, 82, 80, 82, 79, 82, 82, 80, 82, 82, 68, 82, 82, 80, 82, 82, 81, 76, 82, 82, 76, 81, 82, 82, 1, 82, 1, 77, 215176774, 78, 115, 76, 1583141547, 77, 2462589768, 78, 0, 60, 106, 2950320814, 66, 19, 60, 82, 82, 5910, 83, 61, 82, 61, 81, 81, 61, 0, 82, 82, 19, 81, 81, 81, 82, 81, 81, 1, 69, 81, 19, 17, 81, 82, 82, 62, 81, 64, 62, 2, 81, 82, 81, 79, 81, 82, 82, 81, 82, 65, 82, 81, 81, 81, 79, 82, 79, 82, 82, 81, 62, 82, 79, 81, 81, 81, 79, 2909258997, 107, 82, 82, 115, 76, 2999745255, 80, 4003010753, 66, 80, 76, 82, 81, 5982, 83, 80, 82, 70, 82, 79, 81, 80, 82, 81, 80, 82, 81, 68, 82, 82, 80, 81, 81, 82, 76, 82, 81, 76, 81, 82, 81, 0, 82, 1, 77, 4153872237, 78, 115, 76, 1583902121, 77, 3801655639, 78, 0, 63, 108, 2849174419, 66, 19, 63, 81, 81, 6031, 83, 64, 81, 61, 81, 82, 64, 1, 81, 81, 19, 82, 82, 81, 82, 81, 81, 2, 69, 82, 19, 17, 82, 81, 82, 65, 81, 64, 65, 2, 81, 81, 82, 79, 81, 82, 82, 81, 82, 65, 81, 82, 82, 81, 79, 82, 79, 81, 82, 81, 62, 81, 79, 82, 81, 82, 79, 1496744731, 109, 82, 82, 115, 76, 319841889, 80, 1323106375, 66, 80, 76, 81, 81, 6103, 83, 80, 81, 70, 81, 79, 82, 80, 81, 82, 80, 81, 81, 68, 81, 82, 80, 82, 82, 82, 76, 81, 82, 76, 82, 82, 81, 0, 82, 1, 77, 1247810432, 78, 115, 76, 1699872377, 77, 662413387, 78, 0, 66, 110, 3999028780, 66, 19, 66, 82, 81, 6152, 83, 67, 82, 61, 82, 82, 67, 0, 82, 82, 19, 81, 82, 82, 82, 81, 82, 0, 69, 81, 19, 17, 81, 82, 82, 68, 81, 64, 68, 0, 81, 81, 82, 79, 81, 82, 82, 82, 82, 65, 81, 82, 81, 82, 79, 81, 79, 82, 81, 81, 62, 81, 79, 82, 82, 81, 79, 3963650001, 111, 81, 81, 115, 76, 1628155827, 80, 2631420917, 66, 80, 76, 82, 81, 6224, 83, 80, 81, 70, 82, 79, 81, 80, 81, 82, 80, 82, 82, 68, 81, 81, 80, 82, 81, 82, 76, 82, 82, 76, 82, 81, 81, 0, 81, 1, 77, 2027426698, 78, 115, 76, 3059058816, 77, 1230103763, 78, 0, 69, 112, 2533941449, 68, 82, 82, 69, 81, 82, 81, 70, 82, 82, 19, 81, 82, 82, 2, 82, 61, 81, 82, 70, 2, 81, 82, 19, 81, 81, 81, 82, 82, 81, 1, 69, 81, 19, 17, 82, 82, 81, 71, 81, 64, 71, 2, 82, 82, 82, 79, 81, 82, 82, 82, 82, 65, 81, 82, 81, 81, 79, 81, 79, 82, 82, 82, 115, 76, 2321389399, 80, 3324654549, 66, 80, 76, 81, 82, 6341, 83, 80, 81, 67, 113, 81, 80, 79, 80, 81, 81, 1436761637, 81, 81, 82, 81, 82, 68, 82, 82, 80, 82, 81, 82, 76, 82, 81, 76, 82, 81, 82, 1, 82, 1, 77, 1386247426, 78, 115, 76, 3193263957, 77, 2077527882, 78, 0, 72, 114, 2774414194, 66, 19, 72, 81, 81, 6394, 83, 73, 81, 61, 81, 81, 73, 0, 81, 82, 19, 81, 81, 81, 81, 81, 81, 0, 69, 81, 19, 17, 81, 81, 81, 74, 81, 64, 74, 2, 81, 81, 81, 79, 82, 82, 81, 82, 82, 65, 82, 82, 82, 81, 79, 81, 79, 82, 81, 81, 62, 82, 79, 81, 82, 82, 79, 1509456154, 115, 82, 81, 115, 76, 186630056, 80, 1189895112, 66, 80, 76, 81, 81, 6466, 83, 80, 82, 70, 82, 79, 81, 80, 81, 82, 80, 81, 81, 68, 82, 82, 80, 81, 81, 81, 76, 81, 81, 76, 81, 81, 82, 1, 81, 1, 77, 3834082981, 78, 115, 76, 439621017, 77, 3820781604, 78, 115, 76, 1993969748, 77, 2869892222, 78, 0, 85, 12, 2590479903, 61, 81, 81, 85, 2, 81, 82, 5, 82, 81, 81, 82, 81, 81, 2, 1, 75, 44757, 66, 17, 75, 81, 82, 6539, 83, 17, 82, 115, 76, 4250080538, 77, 3889194177, 78, 1, 75, 2772, 66, 17, 75, 81, 81, 6557, 83, 17, 81, 115, 76, 2559456093, 77, 2237611151, 78, 68, 82, 82, 13, 81, 82, 82, 85, 82, 82, 5, 82, 82, 82, 0, 82, 43, 12, 3, 11, 1, 85, 120, 14, 116, 653255872, 75, 12517, 68, 82, 82, 75, 81, 81, 81, 17, 82, 82, 17, 82, 82, 82, 0, 81, 115, 76, 1553317913, 77, 2301044337, 78, 72, 85, 81, 84, 81, 84, 14, 81, 6624, 6624, 12, 73, 81, 85, 81, 82, 10, 81, 81, 86, 43, 9, 7, 8, 1, 86, 1, 75, 11594, 68, 82, 82, 75, 82, 82, 82, 17, 82, 81, 17, 81, 81, 82, 0, 81, 115, 76, 3617385844, 77, 213698350, 78, 0, 85, 117, 780781289, 111, 120, 13, 5, 217092358, 75, 1842, 68, 81, 81, 75, 82, 81, 82, 17, 81, 81, 17, 81, 82, 81, 1, 82, 115, 76, 3528067165, 77, 1665815084, 78, 1, 75, 35911, 66, 17, 75, 81, 81, 6709, 83, 17, 81, 115, 76, 282282620, 77, 2817178699, 78, 0, 85, 8, 0, 112, 121, 85, 117, 780781289, 86, 3, 85, 43, 10, 3, 86, 1, 5, 1, 75, 20593, 68, 81, 82, 75, 82, 82, 82, 17, 81, 81, 17, 82, 81, 81, 0, 81, 115, 76, 3384108889, 77, 419011751, 78, 1, 75, 8326, 66, 17, 75, 82, 82, 6770, 83, 17, 82, 115, 76, 80147108, 77, 2713616453, 78, 1, 75, 57054, 71, 81, 17, 75, 81, 75, 82, 118, 81, 732247888, 64, 75, 2, 82, 81, 82, 79, 81, 81, 81, 81, 82, 65, 81, 81, 81, 82, 79, 81, 79, 81, 81, 81, 62, 81, 79, 81, 82, 81, 79, 2136194269, 119, 82, 82, 115, 76, 116304504, 80, 1401475385, 66, 80, 76, 82, 81, 6837, 83, 80, 81, 70, 82, 79, 81, 80, 82, 81, 80, 81, 81, 68, 82, 81, 80, 81, 81, 81, 76, 81, 81, 76, 81, 82, 82, 1, 81, 1, 77, 589731274, 78, 1, 75, 15808, 66, 17, 75, 81, 81, 6879, 83, 17, 81, 115, 76, 2628203038, 77, 2447353361, 78, 1, 75, 16563, 68, 81, 82, 75, 81, 82, 82, 17, 82, 81, 17, 81, 82, 81, 0, 82, 115, 76, 1146312644, 77, 1743810917, 78, 121, 85, 120, 3808319268, 86, 3, 85, 71, 81, 5, 85, 81, 86, 81, 121, 81, 3622056223, 64, 85, 2, 81, 81, 81, 79, 81, 81, 81, 81, 82, 65, 81, 81, 81, 81, 79, 82, 79, 81, 81, 81, 115, 76, 3523921904, 80, 514126513, 66, 80, 76, 82, 82, 6964, 83, 80, 82, 67, 122, 81, 80, 79, 80, 81, 81, 98544709, 81, 81, 81, 81, 81, 68, 81, 81, 80, 81, 82, 82, 76, 82, 81, 76, 82, 81, 82, 1, 82, 1, 77, 3079068897, 78, 1, 75, 17629, 66, 17, 75, 81, 82, 7010, 83, 17, 82, 115, 76, 3823342130, 77, 806288820, 78, 1, 75, 9251, 66, 17, 75, 81, 81, 7028, 83, 17, 81, 115, 76, 326626894, 77, 4198292044, 78, 61, 81, 82, 6, 1, 81, 81, 3, 81, 81, 81, 82, 81, 82, 0, 0, 85, 123, 380468991, 61, 81, 81, 85, 2, 81, 81, 4, 81, 81, 81, 81, 81, 81, 1, 1, 75, 21629, 68, 82, 81, 75, 81, 82, 81, 17, 81, 81, 17, 81, 81, 82, 1, 82, 115, 76, 2101312995, 77, 3724923009, 78, 1, 75, 12513, 71, 82, 17, 75, 81, 75, 82, 124, 81, 553900710, 64, 75, 2, 82, 82, 81, 79, 82, 82, 82, 82, 82, 65, 81, 81, 81, 81, 79, 82, 79, 82, 81, 81, 115, 76, 260356164, 80, 2494574351, 66, 80, 76, 82, 82, 7143, 83, 80, 82, 67, 125, 81, 80, 80, 79, 82, 81, 2194632658, 82, 82, 82, 81, 81, 68, 82, 81, 80, 81, 81, 82, 76, 81, 81, 76, 81, 81, 81, 0, 82, 1, 77, 2432974941, 78, 1, 75, 18012, 68, 82, 82, 75, 82, 81, 82, 17, 82, 82, 17, 82, 82, 82, 1, 82, 115, 76, 1847357423, 77, 1283240058, 78, 1, 75, 2495, 68, 81, 82, 75, 82, 82, 82, 17, 81, 82, 17, 82, 81, 82, 0, 82, 115, 76, 2830300776, 77, 2546038551, 78, 68, 82, 82, 9, 81, 81, 82, 85, 81, 82, 4, 82, 82, 82, 0, 82, 61, 82, 82, 85, 1, 81, 82, 4, 82, 82, 81, 81, 81, 82, 2, 1, 75, 11968, 66, 17, 75, 81, 82, 7270, 83, 17, 81, 115, 76, 1708795755, 77, 727211509, 78, 0, 85, 126, 3016333302, 68, 82, 82, 85, 82, 81, 82, 86, 81, 82, 5, 81, 82, 82, 0, 81, 61, 82, 82, 86, 2, 81, 81, 5, 81, 82, 81, 81, 82, 81, 0, 1, 75, 29502, 66, 17, 75, 82, 81, 7323, 83, 17, 82, 115, 76, 3213938418, 77, 112055842, 78, 122, 7, 127, 2895785760, 85, 128, 903048147, 8, 7, 85, 1, 75, 4175, 68, 81, 81, 75, 82, 81, 82, 17, 81, 81, 17, 81, 82, 81, 1, 82, 115, 76, 1968704196, 77, 3930739581, 78, 4, 2, 2, 85, 129, 3736232219, 42, 6, 85, 1, 0, 1, 75, 17332, 68, 82, 82, 75, 82, 81, 81, 17, 82, 81, 17, 81, 82, 81, 0, 82, 115, 76, 3176666447, 77, 3931111201, 78, 45, 4, 42, 76, 78, 2, 77, 76, 113, 128, 85, 12, 2590479903, 83, 130, 1601296599, 84, 131, 2654068752, 82, 132, 2747247100, 81, 133, 2150161689, 47, 78, 11000, 3, 16, 0, 0, 125, 20, 21, 134, 2276033863, 61, 83, 83, 21, 0, 81, 83, 22, 83, 82, 83, 81, 83, 81, 0, 0, 23, 12, 2590479903, 61, 82, 83, 23, 1, 81, 83, 24, 81, 81, 83, 83, 81, 83, 2, 0, 25, 135, 2561454488, 63, 25, 26, 81, 83, 83, 83, 82, 22, 64, 26, 1, 83, 83, 81, 79, 83, 82, 82, 83, 82, 65, 82, 81, 82, 81, 79, 82, 79, 83, 82, 82, 62, 82, 79, 81, 82, 83, 79, 274308040, 136, 82, 81, 115, 75, 3958557058, 80, 822972862, 66, 80, 75, 82, 83, 7532, 84, 80, 81, 70, 81, 80, 81, 79, 81, 81, 80, 81, 82, 68, 81, 83, 80, 82, 81, 82, 75, 82, 81, 75, 81, 81, 81, 0, 83, 115, 76, 11055, 77, 13440, 79, 0, 27, 137, 1339302454, 61, 83, 82, 27, 0, 81, 82, 24, 81, 81, 81, 82, 81, 81, 0, 69, 81, 24, 22, 81, 83, 82, 28, 83, 64, 28, 1, 81, 82, 81, 79, 82, 81, 81, 81, 83, 65, 83, 82, 83, 83, 79, 81, 79, 83, 83, 81, 115, 75, 539144263, 80, 2056065599, 66, 80, 75, 83, 82, 7629, 84, 80, 83, 67, 138, 82, 80, 80, 79, 82, 83, 860962044, 81, 83, 82, 83, 81, 68, 83, 82, 80, 83, 81, 82, 75, 82, 82, 75, 83, 82, 81, 0, 83, 115, 76, 29291, 77, 22220, 79, 116, 75, 1724928405, 76, 52234, 77, 29746, 79, 0, 29, 139, 2824434976, 68, 83, 82, 29, 82, 82, 81, 30, 82, 82, 24, 82, 82, 82, 2, 81, 61, 81, 82, 30, 0, 81, 81, 24, 83, 81, 81, 82, 81, 83, 2, 69, 82, 24, 22, 83, 82, 83, 31, 83, 64, 31, 0, 81, 83, 83, 79, 81, 82, 82, 82, 82, 65, 81, 82, 82, 83, 79, 82, 79, 81, 82, 83, 62, 83, 79, 82, 83, 82, 79, 1884501233, 140, 83, 82, 115, 75, 2335882099, 80, 3852803435, 66, 80, 75, 82, 82, 7765, 84, 80, 81, 70, 82, 80, 83, 79, 82, 83, 80, 81, 83, 68, 83, 83, 80, 83, 81, 81, 75, 83, 83, 75, 82, 82, 82, 2, 82, 115, 76, 56091, 77, 6148, 79, 116, 75, 53416382, 76, 15472, 77, 49610, 79, 0, 32, 141, 1549502091, 66, 24, 32, 82, 82, 7818, 84, 33, 82, 61, 83, 82, 33, 2, 83, 82, 24, 83, 82, 82, 82, 83, 82, 1, 69, 82, 24, 22, 83, 81, 82, 34, 82, 64, 34, 0, 81, 83, 83, 79, 83, 83, 82, 83, 83, 65, 81, 82, 83, 82, 79, 82, 79, 83, 82, 81, 62, 82, 79, 83, 82, 81, 79, 2356301449, 142, 81, 81, 115, 75, 3196139735, 80, 418093775, 66, 80, 75, 83, 82, 7890, 84, 80, 83, 70, 83, 79, 82, 80, 83, 82, 80, 83, 81, 68, 81, 81, 80, 82, 83, 81, 75, 82, 81, 75, 82, 81, 82, 0, 82, 115, 76, 9924, 77, 9551, 79, 116, 75, 3504905719, 76, 19704, 77, 20457, 79, 0, 35, 143, 802882934, 66, 24, 35, 81, 82, 7943, 84, 36, 82, 61, 83, 81, 36, 1, 82, 81, 24, 83, 81, 82, 81, 83, 82, 1, 69, 81, 24, 22, 82, 82, 83, 37, 82, 64, 37, 1, 81, 81, 82, 79, 81, 83, 83, 81, 81, 65, 81, 81, 81, 81, 79, 82, 79, 82, 83, 81, 62, 81, 79, 81, 81, 81, 79, 1321750434, 144, 82, 81, 115, 75, 2627979969, 80, 4234845049, 66, 80, 75, 81, 82, 8015, 84, 80, 81, 70, 81, 79, 81, 80, 81, 81, 80, 81, 81, 68, 83, 81, 80, 81, 81, 83, 75, 83, 81, 75, 81, 82, 82, 2, 81, 115, 76, 50755, 77, 47008, 79, 116, 75, 412203078, 76, 55060, 77, 7814, 79, 0, 38, 145, 3970351606, 68, 82, 82, 38, 82, 83, 82, 39, 82, 81, 24, 83, 82, 81, 0, 83, 61, 83, 83, 39, 0, 82, 81, 24, 81, 82, 82, 81, 83, 81, 2, 69, 83, 24, 22, 83, 83, 81, 40, 81, 64, 40, 0, 81, 81, 81, 79, 82, 81, 83, 81, 81, 65, 83, 82, 81, 82, 79, 83, 79, 82, 82, 81, 115, 75, 1269327000, 80, 2876192080, 66, 80, 75, 82, 81, 8136, 84, 80, 82, 67, 146, 83, 80, 79, 80, 83, 81, 3203207074, 82, 82, 83, 82, 82, 68, 83, 83, 80, 82, 83, 82, 75, 82, 81, 75, 83, 83, 81, 0, 81, 115, 76, 20857, 77, 11413, 79, 116, 75, 934233576, 76, 50098, 77, 306, 79, 0, 41, 147, 1071677370, 68, 82, 81, 41, 82, 82, 83, 42, 82, 82, 24, 83, 83, 82, 0, 81, 61, 82, 82, 42, 1, 81, 81, 24, 81, 81, 82, 81, 83, 82, 2, 69, 81, 24, 22, 81, 82, 81, 43, 81, 64, 43, 0, 81, 82, 82, 79, 82, 81, 81, 82, 82, 65, 82, 81, 82, 81, 79, 81, 79, 82, 82, 81, 62, 81, 79, 82, 82, 82, 79, 320185709, 148, 83, 82, 115, 75, 2552338073, 80, 4159203153, 66, 80, 75, 82, 82, 8272, 84, 80, 83, 70, 83, 80, 83, 79, 81, 82, 80, 82, 81, 68, 83, 81, 80, 82, 82, 82, 75, 81, 81, 75, 83, 81, 81, 0, 82, 115, 76, 37716, 77, 3180, 79, 116, 75, 2374014349, 76, 10080, 77, 55309, 79, 0, 44, 149, 1257454737, 68, 83, 83, 44, 81, 82, 83, 45, 81, 83, 24, 83, 82, 83, 2, 82, 61, 82, 83, 45, 0, 81, 82, 24, 81, 81, 81, 81, 83, 83, 2, 69, 81, 24, 22, 82, 83, 81, 46, 81, 64, 46, 1, 83, 83, 83, 79, 83, 81, 81, 83, 81, 65, 81, 82, 81, 82, 79, 83, 79, 81, 83, 83, 62, 81, 79, 83, 83, 81, 79, 2681863755, 150, 82, 83, 115, 75, 433991685, 80, 1950913021, 66, 80, 75, 81, 82, 8404, 84, 80, 81, 70, 81, 79, 83, 80, 81, 82, 80, 81, 82, 68, 83, 81, 80, 81, 82, 82, 75, 83, 83, 75, 83, 81, 83, 1, 81, 115, 76, 28100, 77, 46848, 79, 116, 75, 1648344015, 76, 55682, 77, 59154, 79, 0, 47, 151, 3934276565, 68, 83, 82, 47, 82, 82, 82, 48, 81, 82, 24, 82, 81, 82, 0, 82, 61, 83, 82, 48, 2, 82, 82, 24, 81, 82, 82, 82, 82, 82, 1, 69, 82, 24, 22, 83, 82, 81, 49, 83, 64, 49, 1, 82, 81, 83, 79, 82, 82, 83, 82, 81, 65, 82, 82, 81, 82, 79, 83, 79, 81, 81, 83, 62, 81, 79, 82, 82, 83, 79, 678648647, 152, 82, 82, 115, 75, 1082437362, 80, 3079058666, 66, 80, 75, 82, 81, 8536, 84, 80, 82, 70, 82, 80, 82, 79, 83, 82, 80, 82, 82, 68, 83, 83, 80, 81, 82, 82, 75, 83, 82, 75, 82, 83, 82, 0, 83, 115, 76, 25267, 77, 48298, 79, 116, 75, 2582472959, 76, 26285, 77, 35020, 79, 0, 50, 153, 854731201, 66, 24, 50, 83, 82, 8589, 84, 51, 82, 61, 83, 83, 51, 0, 82, 82, 24, 82, 82, 82, 82, 82, 82, 2, 69, 82, 24, 22, 82, 82, 82, 52, 82, 64, 52, 0, 82, 81, 82, 79, 82, 81, 83, 83, 83, 65, 82, 81, 82, 83, 79, 82, 79, 82, 81, 83, 115, 75, 384209151, 80, 1991074231, 66, 80, 75, 83, 81, 8650, 84, 80, 82, 67, 154, 82, 80, 80, 79, 83, 82, 2350804338, 81, 81, 83, 82, 82, 68, 82, 82, 80, 83, 82, 81, 75, 82, 83, 75, 82, 82, 82, 2, 83, 115, 76, 43840, 77, 34708, 79, 116, 75, 1905234457, 76, 44224, 77, 31173, 79, 0, 53, 155, 1492218370, 68, 82, 81, 53, 81, 82, 82, 54, 81, 82, 24, 82, 82, 83, 2, 81, 61, 81, 82, 54, 2, 82, 81, 24, 81, 83, 83, 82, 81, 82, 0, 69, 81, 24, 22, 81, 81, 81, 55, 81, 64, 55, 0, 82, 83, 81, 79, 83, 81, 81, 81, 81, 65, 81, 81, 83, 81, 79, 81, 79, 81, 83, 81, 115, 75, 212808855, 80, 1819673935, 66, 80, 75, 81, 81, 8775, 84, 80, 83, 67, 156, 81, 80, 79, 80, 82, 82, 4179346145, 81, 81, 83, 81, 83, 68, 83, 82, 80, 82, 81, 82, 75, 82, 81, 75, 82, 81, 81, 0, 83, 115, 76, 14609, 77, 576, 79, 116, 75, 2529946988, 76, 20079, 77, 53112, 79, 0, 56, 157, 1011163784, 68, 83, 83, 56, 82, 83, 83, 57, 83, 83, 24, 81, 82, 83, 2, 82, 61, 83, 82, 57, 1, 82, 83, 24, 81, 83, 81, 82, 83, 82, 0, 69, 81, 24, 22, 82, 81, 81, 58, 83, 64, 58, 1, 82, 83, 83, 79, 81, 83, 83, 82, 81, 65, 82, 82, 81, 81, 79, 82, 79, 82, 83, 82, 62, 81, 79, 81, 83, 83, 79, 1221984254, 158, 81, 83, 115, 75, 3693950684, 80, 1005848468, 66, 80, 75, 81, 81, 8911, 84, 80, 82, 70, 81, 80, 83, 79, 82, 81, 80, 82, 82, 68, 82, 82, 80, 81, 82, 81, 75, 82, 83, 75, 81, 83, 81, 2, 82, 115, 76, 43688, 77, 24338, 79, 116, 75, 143571247, 76, 10989, 77, 46587, 79, 0, 59, 159, 866245919, 66, 24, 59, 83, 83, 8964, 84, 60, 83, 61, 81, 83, 60, 0, 81, 82, 24, 83, 81, 83, 81, 83, 81, 1, 69, 83, 24, 22, 83, 83, 81, 61, 81, 64, 61, 2, 83, 83, 81, 79, 83, 83, 83, 83, 82, 65, 83, 83, 83, 83, 79, 82, 79, 83, 83, 83, 62, 83, 79, 81, 81, 82, 79, 3353700522, 160, 83, 81, 115, 75, 861388120, 80, 2468253200, 66, 80, 75, 83, 82, 9036, 84, 80, 81, 70, 82, 80, 82, 79, 83, 83, 80, 81, 83, 68, 83, 82, 80, 83, 81, 83, 75, 83, 81, 75, 83, 82, 82, 0, 82, 115, 76, 60729, 77, 18552, 79, 116, 75, 1790924193, 76, 54579, 77, 56948, 79, 0, 62, 161, 1313205680, 68, 83, 82, 62, 82, 82, 83, 63, 81, 82, 24, 81, 83, 82, 1, 81, 61, 83, 82, 63, 1, 82, 81, 24, 83, 82, 82, 82, 82, 83, 2, 69, 83, 24, 22, 81, 82, 82, 64, 82, 64, 64, 1, 82, 82, 83, 79, 82, 83, 82, 82, 83, 65, 83, 83, 83, 81, 79, 82, 79, 83, 82, 82, 62, 83, 79, 82, 81, 83, 79, 2113629223, 162, 82, 81, 115, 75, 1797763611, 80, 3404628691, 66, 80, 75, 83, 82, 9168, 84, 80, 81, 70, 81, 80, 83, 79, 83, 83, 80, 83, 83, 68, 83, 81, 80, 82, 81, 81, 75, 81, 81, 75, 81, 81, 82, 1, 81, 115, 76, 59474, 77, 1310, 79, 116, 75, 415324648, 76, 56755, 77, 30364, 79, 0, 65, 163, 1409999165, 66, 24, 65, 81, 81, 9221, 84, 66, 81, 61, 82, 83, 66, 0, 81, 81, 24, 82, 83, 81, 81, 82, 81, 2, 69, 81, 24, 22, 82, 81, 82, 67, 82, 64, 67, 2, 82, 83, 81, 79, 82, 81, 81, 82, 81, 65, 81, 81, 81, 81, 79, 81, 79, 83, 82, 82, 115, 75, 666412539, 80, 2273277619, 66, 80, 75, 81, 81, 9282, 84, 80, 81, 67, 164, 83, 80, 79, 80, 81, 81, 4073475186, 83, 81, 83, 83, 82, 68, 81, 81, 80, 82, 83, 83, 75, 83, 83, 75, 83, 81, 83, 0, 81, 115, 76, 31500, 77, 52546, 79, 116, 75, 2759462425, 76, 60348, 77, 51500, 79, 0, 68, 165, 3028697801, 66, 24, 68, 81, 83, 9339, 84, 69, 81, 61, 82, 83, 69, 0, 81, 82, 24, 83, 81, 81, 82, 81, 82, 2, 69, 83, 24, 22, 82, 82, 82, 70, 81, 64, 70, 2, 82, 82, 81, 79, 83, 82, 81, 82, 81, 65, 81, 81, 81, 81, 79, 82, 79, 82, 82, 82, 115, 75, 359345528, 80, 1966210608, 66, 80, 75, 83, 82, 9400, 84, 80, 82, 67, 166, 82, 80, 80, 79, 82, 83, 2300116243, 83, 81, 82, 82, 82, 68, 81, 82, 80, 82, 82, 83, 75, 81, 83, 75, 81, 82, 81, 1, 81, 115, 76, 31273, 77, 25668, 79, 116, 75, 608210523, 76, 47908, 77, 15619, 79, 0, 71, 167, 2315330412, 68, 83, 83, 71, 82, 83, 82, 72, 83, 83, 24, 83, 82, 83, 2, 81, 61, 82, 83, 72, 0, 82, 82, 24, 83, 81, 83, 81, 82, 82, 1, 69, 82, 24, 22, 81, 82, 82, 73, 83, 64, 73, 2, 82, 82, 83, 79, 82, 81, 82, 81, 83, 65, 81, 83, 81, 83, 79, 82, 79, 83, 82, 83, 62, 83, 79, 83, 83, 83, 79, 2060335630, 168, 83, 83, 115, 75, 2079454373, 80, 3686319453, 66, 80, 75, 83, 83, 9536, 84, 80, 83, 70, 83, 80, 81, 79, 83, 83, 80, 83, 82, 68, 81, 82, 80, 82, 82, 82, 75, 82, 82, 75, 82, 82, 82, 0, 82, 115, 76, 9944, 77, 10766, 79, 116, 75, 1156761045, 76, 20948, 77, 59550, 79, 116, 75, 1582886263, 76, 22349, 77, 58838, 79, 125, 5, 86, 123, 380468991, 61, 82, 83, 86, 0, 83, 83, 6, 81, 83, 82, 83, 82, 83, 1, 1, 74, 9323, 68, 81, 82, 74, 82, 81, 82, 22, 81, 81, 22, 83, 83, 83, 2, 83, 116, 75, 3890099062, 76, 4430, 77, 23633, 79, 8, 86, 0, 16, 68, 82, 82, 7, 82, 82, 81, 87, 82, 82, 2, 81, 81, 82, 1, 81, 43, 17, 0, 86, 1, 87, 1, 74, 51817, 68, 81, 82, 74, 82, 82, 82, 22, 82, 81, 22, 82, 81, 83, 1, 81, 116, 75, 579842019, 76, 38113, 77, 8607, 79, 61, 83, 81, 19, 0, 82, 82, 7, 81, 81, 81, 82, 82, 82, 1, 1, 74, 13220, 68, 83, 83, 74, 82, 83, 83, 22, 83, 83, 22, 83, 83, 83, 0, 83, 116, 75, 4170907689, 76, 17563, 77, 19348, 79, 0, 86, 12, 2590479903, 61, 83, 83, 86, 1, 81, 81, 7, 81, 83, 83, 82, 83, 82, 1, 1, 74, 15143, 68, 82, 82, 74, 81, 81, 82, 22, 81, 81, 22, 81, 81, 81, 1, 81, 116, 75, 4195530643, 76, 36145, 77, 18972, 79, 45, 6, 73, 81, 10, 82, 83, 9, 83, 81, 86, 61, 82, 83, 86, 2, 82, 81, 1, 83, 81, 83, 81, 83, 82, 2, 120, 11, 169, 692561464, 74, 16750, 68, 82, 82, 74, 83, 82, 83, 22, 83, 83, 22, 83, 83, 82, 2, 81, 116, 75, 2232908577, 76, 51619, 77, 48919, 79, 123, 86, 128, 903048147, 15, 14, 86, 16, 117, 780781289, 1, 74, 908, 68, 82, 81, 74, 81, 82, 81, 22, 82, 81, 22, 81, 81, 81, 1, 82, 116, 75, 4260880831, 76, 25349, 77, 17975, 79, 61, 83, 81, 7, 1, 81, 81, 86, 83, 81, 81, 81, 82, 81, 1, 0, 86, 5, 217092358, 68, 83, 81, 86, 82, 83, 83, 19, 81, 83, 7, 81, 83, 82, 2, 82, 1, 74, 23639, 68, 83, 82, 74, 83, 81, 82, 22, 82, 81, 22, 83, 83, 83, 0, 81, 116, 75, 691741329, 76, 8755, 77, 6900, 79, 0, 86, 170, 60698607, 68, 83, 82, 86, 82, 83, 81, 9, 81, 81, 1, 81, 82, 81, 2, 82, 120, 10, 12, 2590479903, 74, 58217, 66, 22, 74, 83, 82, 9962, 84, 22, 83, 116, 75, 1348984699, 76, 25929, 77, 62407, 79, 61, 83, 83, 18, 2, 82, 81, 6, 82, 82, 81, 81, 82, 81, 0, 1, 74, 16233, 66, 22, 74, 81, 83, 9997, 84, 22, 82, 116, 75, 3459572068, 76, 25440, 77, 30202, 79, 74, 81, 86, 81, 81, 8, 81, 82, 82, 17, 43, 87, 14, 15, 1, 86, 68, 82, 81, 87, 81, 81, 83, 18, 82, 83, 6, 81, 82, 81, 1, 82, 1, 74, 23196, 66, 22, 74, 81, 81, 10049, 84, 22, 81, 116, 75, 1657590089, 76, 29563, 77, 57698, 79, 127, 86, 8, 0, 86, 71, 81, 7, 86, 83, 3, 82, 171, 81, 2491504093, 64, 86, 0, 81, 81, 81, 79, 81, 83, 82, 81, 81, 65, 83, 83, 83, 81, 79, 82, 79, 83, 83, 81, 62, 82, 79, 83, 81, 82, 79, 3190307608, 172, 83, 83, 115, 75, 945591356, 80, 1193414704, 66, 80, 75, 83, 82, 10120, 84, 80, 82, 70, 83, 79, 82, 80, 81, 81, 80, 83, 83, 68, 83, 83, 80, 83, 81, 83, 75, 83, 83, 75, 83, 83, 83, 0, 83, 115, 76, 39749, 77, 43772, 79, 1, 74, 19026, 66, 22, 74, 82, 83, 10164, 84, 22, 83, 116, 75, 1750049717, 76, 41231, 77, 56287, 79, 1, 74, 10443, 68, 81, 81, 74, 81, 81, 81, 22, 81, 81, 22, 82, 81, 81, 1, 82, 116, 75, 2539490361, 76, 47243, 77, 44241, 79, 70, 82, 12, 81, 13, 82, 82, 86, 82, 81, 61, 82, 82, 86, 0, 82, 82, 8, 83, 82, 82, 82, 81, 83, 0, 2, 14, 127, 2895785760, 1, 74, 17073, 66, 22, 74, 82, 81, 10240, 84, 22, 81, 116, 75, 1268954953, 76, 36219, 77, 63628, 79, 75, 11, 42364, 1698447868, 81, 83, 1, 86, 173, 74, 81, 12, 83, 83, 86, 83, 82, 82, 1, 120, 13, 174, 1960272759, 74, 2058, 66, 22, 74, 81, 81, 10282, 84, 22, 83, 116, 75, 510740409, 76, 64779, 77, 9661, 79, 1, 74, 17876, 68, 82, 81, 74, 83, 81, 83, 22, 81, 82, 22, 82, 81, 82, 0, 83, 116, 75, 2050720350, 76, 49766, 77, 51699, 79, 42, 75, 78, 3, 75, 76, 77, 114, 128, 8, 12, 2590479903, 4, 175, 2738340406, 7, 176, 3768116900, 5, 177, 568884116, 6, 178, 3139197788, 125, 3, 9, 179, 2814565029, 74, 5, 10, 4, 5, 9, 5, 4, 4, 0, 0, 9, 12, 2590479903, 75, 9, 8112, 1984922113, 6, 6, 10, 11, 180, 61, 6, 6, 11, 1, 6, 5, 0, 6, 4, 6, 6, 6, 6, 1, 68, 5, 6, 1, 5, 4, 5, 9, 5, 4, 0, 6, 6, 4, 1, 5, 0, 10, 12, 2590479903, 75, 10, 25280, 3567045076, 4, 4, 9, 11, 181, 61, 5, 6, 11, 0, 6, 5, 0, 5, 6, 6, 6, 6, 6, 2, 130, 9, 182, 3749019917, 10, 183, 2858810407, 11, 9, 10, 12, 184, 2165135928, 13, 9, 11, 2, 0, 12, 0, 9, 12, 2590479903, 75, 9, 45410, 2890770946, 6, 4, 13, 10, 185, 61, 5, 5, 10, 2, 5, 6, 0, 6, 6, 6, 6, 4, 6, 1, 0, 9, 186, 1111237406, 75, 9, 57156, 802811302, 4, 5, 0, 10, 187, 74, 4, 9, 6, 4, 10, 6, 6, 5, 0, 0, 10, 12, 2590479903, 75, 10, 24688, 1800546891, 5, 4, 9, 11, 188, 61, 5, 5, 11, 2, 4, 4, 0, 4, 6, 4, 6, 4, 5, 2, 76, 9, 6, 0, 5, 4, 6, 4, 6, 5, 6, 0, 10, 12, 2590479903, 75, 10, 1619, 235364115, 6, 4, 9, 11, 189, 61, 4, 6, 11, 1, 6, 5, 0, 4, 6, 4, 5, 6, 4, 1, 0, 9, 190, 2174882816, 72, 10, 6, 8, 5, 8, 9, 5, 10580, 10580, 0, 0, 9, 191, 3692211810, 75, 9, 22569, 1574396925, 4, 6, 0, 11, 192, 73, 5, 11, 4, 5, 10, 4, 5, 9, 0, 10, 12, 2590479903, 75, 10, 52458, 961733991, 5, 5, 9, 11, 193, 61, 4, 6, 11, 0, 5, 4, 0, 6, 6, 6, 4, 4, 5, 2, 0, 9, 194, 4106638508, 68, 6, 6, 9, 5, 6, 5, 10, 5, 4, 0, 5, 6, 5, 0, 4, 0, 9, 12, 2590479903, 75, 9, 2808, 1968985494, 6, 5, 10, 11, 195, 61, 4, 4, 11, 1, 4, 6, 0, 5, 4, 6, 5, 4, 4, 0, 0, 9, 74, 3708911840, 49, 10, 1, 9, 0, 45, 10, 127, 9, 8, 0, 9, 129, 5, 196, 3724882778, 6, 197, 3736194538, 7, 12, 2590479903, 4, 198, 2998004113, 3, 76, 8, 5, 1, 4, 5, 5, 4, 4, 5, 4, 0, 9, 12, 2590479903, 75, 9, 6914, 3252963733, 5, 4, 8, 10, 199, 61, 4, 5, 10, 1, 5, 4, 1, 4, 5, 4, 5, 5, 4, 0, 0, 8, 200, 798324653, 75, 8, 27909, 2280062711, 5, 4, 1, 9, 201, 74, 4, 8, 4, 4, 9, 5, 5, 5, 1, 0, 9, 12, 2590479903, 75, 9, 41067, 4165410136, 4, 4, 8, 10, 202, 61, 4, 4, 10, 0, 4, 4, 1, 5, 4, 4, 5, 4, 4, 0, 0, 8, 203, 2648470929, 72, 9, 4, 7, 4, 7, 8, 4, 10813, 10813, 1, 0, 8, 204, 2346256606, 75, 8, 61909, 2775865948, 5, 4, 1, 10, 205, 73, 5, 10, 4, 4, 9, 5, 4, 8, 0, 9, 12, 2590479903, 75, 9, 35116, 2293319329, 4, 4, 8, 10, 206, 61, 4, 4, 10, 2, 4, 4, 1, 5, 4, 4, 5, 4, 5, 1, 68, 4, 5, 0, 4, 4, 4, 8, 5, 4, 1, 5, 4, 5, 1, 5, 0, 9, 12, 2590479903, 75, 9, 23961, 2942584715, 4, 4, 8, 10, 207, 61, 4, 5, 10, 1, 5, 5, 1, 5, 5, 5, 4, 4, 4, 2, 76, 8, 5, 1, 5, 5, 5, 4, 4, 5, 5, 0, 9, 12, 2590479903, 75, 9, 26081, 1080256578, 5, 4, 8, 10, 208, 61, 4, 4, 10, 0, 5, 5, 1, 4, 5, 5, 4, 4, 5, 0, 130, 8, 182, 3749019917, 9, 183, 2858810407, 10, 8, 9, 11, 209, 2669240111, 12, 8, 10, 2, 1, 11, 0, 8, 12, 2590479903, 75, 8, 52499, 2094220149, 5, 5, 12, 9, 210, 61, 5, 5, 9, 2, 5, 5, 1, 4, 5, 5, 5, 5, 5, 2, 45, 1, 127, 8, 8, 0, 8, 128, 8, 211, 3154102270, 6, 212, 266150813, 10, 12, 2590479903, 7, 213, 728163735, 9, 214, 2280751007, 125, 4, 11, 186, 1111237406, 72, 12, 8, 10, 8, 10, 11, 8, 11032, 11032, 2, 73, 6, 12, 8, 7, 1, 7, 6, 11, 0, 12, 12, 2590479903, 75, 12, 44184, 1089100739, 8, 7, 11, 13, 215, 61, 7, 7, 13, 1, 6, 8, 5, 7, 6, 8, 7, 7, 6, 2, 68, 7, 7, 5, 7, 7, 7, 11, 7, 6, 0, 7, 7, 6, 2, 8, 0, 12, 12, 2590479903, 75, 12, 12225, 1642457825, 8, 8, 11, 13, 216, 61, 8, 8, 13, 0, 6, 6, 0, 7, 6, 6, 8, 8, 7, 0, 0, 11, 217, 3104206784, 66, 0, 11, 8, 8, 11126, 9, 12, 8, 0, 11, 12, 2590479903, 75, 11, 6058, 2690855783, 8, 7, 12, 13, 218, 61, 8, 7, 13, 1, 8, 8, 0, 7, 7, 7, 8, 7, 7, 2, 0, 11, 219, 1135468396, 68, 7, 6, 11, 7, 6, 6, 12, 7, 8, 0, 8, 7, 6, 0, 6, 0, 11, 12, 2590479903, 75, 11, 58287, 3551166585, 7, 6, 12, 13, 220, 61, 7, 6, 13, 2, 6, 8, 0, 6, 6, 7, 7, 6, 6, 1, 130, 11, 182, 3749019917, 12, 183, 2858810407, 13, 11, 12, 14, 221, 3062316541, 15, 11, 13, 2, 0, 14, 0, 11, 12, 2590479903, 75, 11, 33896, 949573256, 8, 8, 15, 12, 222, 61, 8, 7, 12, 1, 8, 8, 0, 6, 7, 7, 6, 8, 8, 2, 0, 11, 223, 1704115429, 75, 11, 5179, 167373368, 6, 7, 0, 12, 224, 74, 8, 11, 6, 6, 12, 8, 6, 6, 0, 0, 12, 12, 2590479903, 75, 12, 59084, 2880807454, 6, 7, 11, 13, 225, 61, 7, 7, 13, 1, 6, 8, 0, 8, 8, 7, 7, 6, 6, 1, 0, 11, 226, 3775380397, 74, 7, 12, 8, 7, 11, 6, 6, 8, 0, 0, 11, 12, 2590479903, 75, 11, 64873, 3705364169, 6, 7, 12, 13, 227, 61, 7, 6, 13, 2, 8, 6, 0, 6, 8, 8, 8, 7, 8, 2, 0, 11, 228, 622406377, 66, 0, 11, 6, 8, 11355, 9, 12, 6, 0, 11, 12, 2590479903, 75, 11, 15885, 1964741709, 7, 7, 12, 13, 229, 61, 6, 7, 13, 1, 7, 6, 0, 8, 7, 8, 7, 7, 7, 1, 45, 0, 127, 11, 8, 0, 11]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 20; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 77: + { + // JUMP_4178 (specialized) + regs[fp + SLOTS.PC] = 4178; + break; + } + case 78: + { + // JUMP_7402 (specialized) + regs[fp + SLOTS.PC] = 7402; + break; + } + case 79: + { + // JUMP_10317 (specialized) + regs[fp + SLOTS.PC] = 10317; + break; + } + case 80: + { + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 81: + { + // LOAD_INT_18_8 (specialized) + var dst = 18; + regs[base + dst] = 8; + break; + } + case 82: + { + // LOAD_CONST_13_0_3547521513 (specialized) + var dst = 13; + regs[base + dst] = this._constant(0, 3547521513); + break; + } + case 83: + { + // LOAD_CONST_14_1_182004232 (specialized) + var dst = 14; + regs[base + dst] = this._constant(1, 182004232); + break; + } + case 84: + { + // LOAD_CONST_12_2_4112486966 (specialized) + var dst = 12; + regs[base + dst] = this._constant(2, 4112486966); + break; + } + case 85: + { + // LOAD_CONST_7_3_3678968651 (specialized) + var dst = 7; + regs[base + dst] = this._constant(3, 3678968651); + break; + } + case 86: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 87: + { + // LOAD_GLOBAL_15_4_520319566 (specialized) + var dst = 15; + var globalName = this._constant(4, 520319566); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 88: + { + // LOAD_INT_9_3487949257 (specialized) + var dst = 9; + regs[base + dst] = 3487949257; + break; + } + case 89: + { + // LOAD_INT_10_186 (specialized) + var dst = 10; + regs[base + dst] = 186; + break; + } + case 90: + { + // LOAD_INT_11_11 (specialized) + var dst = 11; + regs[base + dst] = 11; + break; + } + case 91: + { + // LOAD_GLOBAL_17_4_520319566 (specialized) + var dst = 17; + var globalName = this._constant(4, 520319566); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 92: + { + // GET_PROP_19_17_4 (specialized) + // dst = regs[obj][regs[key]] + var dst = 19; + var obj = regs[base + 17]; + var key = regs[base + 4]; + regs[base + dst] = obj[key]; + break; + } + case 93: + { + // SET_PROP_15_3_17 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 15]; + var key = regs[base + 3]; + var val = regs[base + 17]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 94: + { + // RETURN_15 (specialized) + var retVal = regs[base + 15]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 95: + { + // LOAD_THIS_19 (specialized) + var dst = 19; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 96: + { + // LOAD_GLOBAL_7_64_838365605 (specialized) + var dst = 7; + var globalName = this._constant(64, 838365605); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 97: + { + // LOAD_GLOBAL_11_4_520319566 (specialized) + var dst = 11; + var globalName = this._constant(4, 520319566); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 98: + { + // LOAD_UPVALUE_14_0 (specialized) + var dst = 14; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 99: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 100: + { + // SET_PROP_11_12_13 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 11]; + var key = regs[base + 12]; + var val = regs[base + 13]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 101: + { + // JUMP_REG_4 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 4]; + break; + } + case 102: + { + // GET_PROP_108_3_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 108; + var obj = regs[base + 3]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; + break; + } + case 103: + { + // THROW_5 (specialized) + throw regs[base + 5]; + break; + } + case 104: + { + // THROW_108 (specialized) + throw regs[base + 108]; + break; + } + case 105: + { + // FINALLY_SETUP_3199_4_5_3847 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 3199, + contReg: 4, + payloadReg: 5, + throwPad: 3847 + }); + break; + } + case 106: + { + // TRY_SETUP_3927_3 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 3927, + exceptionReg: 3 + }); + break; + } + case 107: + { + // RETURN_108 (specialized) + var retVal = regs[base + 108]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 108: + { + // GET_PROP_98_98_101 (specialized) + // dst = regs[obj][regs[key]] + var dst = 98; + var obj = regs[base + 98]; + var key = regs[base + 101]; + regs[base + dst] = obj[key]; + break; + } + case 109: + { + // JUMP_REG_98 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 98]; + break; + } + case 110: + { + // LOAD_THIS_15 (specialized) + var dst = 15; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 111: + { + // GET_PROP_11_3_85 (specialized) + // dst = regs[obj][regs[key]] + var dst = 11; + var obj = regs[base + 3]; + var key = regs[base + 85]; + regs[base + dst] = obj[key]; + break; + } + case 112: + { + // RETURN_85 (specialized) + var retVal = regs[base + 85]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 113: + { + // JUMP_REG_76 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 76]; + break; + } + case 114: + { + // JUMP_REG_75 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 75]; + break; + } + case 115: + { + // LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 116: + { + // LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 117: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 118: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 119: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 120: + { + // LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 121: + { + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 122: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 123: + { + // LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 124: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 125: + { + // LOAD_THIS,LOAD_CONST (macro) + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 126: + { + // GET_PROP,LOAD_CONST (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 127: + { + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 128: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 129: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 130: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 131: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 132: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 133: + { + // LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 134: + { + // LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 135: + { + // GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 136: + { + // LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 137: + { + // LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 138: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 139: + { + // LOAD_INT,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 140: + { + // LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 141: + { + // LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 61: + { + // ANTI_MOVE_7_8_13_1_2_6_12_10_0_3_4_11_9_5 (order: [7,8,13,1,2,6,12,10,0,3,4,11,9,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[13], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[12], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[6], _unsortedOperands[2]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[0]; + var pairCount = _operands[1]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[2]]; + var val = regs[base + _operands[3]]; + o[key] = val; + } + regs[base + dst] = o; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[4]; + var count = _operands[5]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[6]]; + regs[base + dst] = elems; + // MOD + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a % regs[base + _operands[9]]; + // MOVE + var dst = _operands[10]; + regs[base + dst] = regs[base + _operands[11]]; + // MOVE + var dst = _operands[12]; + regs[base + dst] = regs[base + _operands[13]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_8_3_6_9_7_4_2_1_0_5 (order: [8,3,6,9,7,4,2,1,0,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[3]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this._constant(_operands[1], _operands[2]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // UNARY_NEG + var dst = _operands[3]; + regs[base + dst] = -regs[base + _operands[4]]; + // UNARY_NEG + var dst = _operands[5]; + regs[base + dst] = -regs[base + _operands[6]]; + // LOOSE_EQ + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a == regs[base + _operands[9]]; + break; + } + case 63: + { + // ANTI_NEQ_4_2_6_0_1_7_5_3 (order: [4,2,6,0,1,7,5,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[5]]; + // UNARY_NOT + var dst = _operands[0]; + regs[base + dst] = !regs[base + _operands[1]]; + // NEQ + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a !== regs[base + _operands[4]]; + // USHR + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a >>> regs[base + _operands[7]]; + break; + } + case 64: + { + // ANTI_UNARY_NOT_4_9_2_5_10_3_6_7_1_0_8 (order: [4,9,2,5,10,3,6,7,1,0,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[4]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // UNARY_NOT + var dst = _operands[3]; + regs[base + dst] = !regs[base + _operands[4]]; + // SHR + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a >> regs[base + _operands[7]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[8]; + var count = _operands[9]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[10]]; + regs[base + dst] = elems; + break; + } + case 65: + { + // ANTI_UNARY_POS_0_5_2_7_4_1_3_6_8_9 (order: [0,5,2,7,4,1,3,6,8,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[9]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[2]]); + // UNARY_POS + var dst = _operands[3]; + regs[base + dst] = +regs[base + _operands[4]]; + // UNARY_POS + var dst = _operands[5]; + regs[base + dst] = +regs[base + _operands[6]]; + // MUL + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a * regs[base + _operands[9]]; + break; + } + case 66: + { + // ANTI_SUB_1_2_6_7_4_3_0_5 (order: [1,2,6,7,4,3,0,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[3]]; + // SUB + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a - regs[base + _operands[2]]; + // JUMP_IF_FALSE + var src = _operands[3]; + var target = _operands[4]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // LOOSE_EQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a == regs[base + _operands[7]]; + break; + } + case 67: + { + // ANTI_MUL_4_3_6_8_7_0_2_5_9_10_11_12_1 (order: [4,3,6,8,7,0,2,5,9,10,11,12,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[12], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[11]]; + // DIV + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a / regs[base + _operands[2]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[3]; + var name = this._constant(_operands[4], _operands[5]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + // UNARY_BITNOT + var dst = _operands[9]; + regs[base + dst] = ~regs[base + _operands[10]]; + // UNARY_BITNOT + var dst = _operands[11]; + regs[base + dst] = ~regs[base + _operands[12]]; + break; + } + case 68: + { + // ANTI_ADD_4_5_8_14_11_9_6_3_2_7_13_10_0_1_12 (order: [4,5,8,14,11,9,6,3,2,7,13,10,0,1,12]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[12], _unsortedOperands[13], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[14], _unsortedOperands[10], _unsortedOperands[3]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[0]; + var count = _operands[1]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[2]]; + regs[base + dst] = elems; + // LOOSE_NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a != regs[base + _operands[5]]; + // ADD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a + regs[base + _operands[8]]; + // LOOSE_NEQ + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a != regs[base + _operands[11]]; + // EQ + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a === regs[base + _operands[14]]; + break; + } + case 69: + { + // ANTI_EQ_4_2_1_5_6_7_0_3 (order: [4,2,1,5,6,7,0,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[5]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // BOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a | regs[base + _operands[5]]; + // UNARY_NEG + var dst = _operands[6]; + regs[base + dst] = -regs[base + _operands[7]]; + break; + } + case 70: + { + // ANTI_BAND_7_1_6_2_3_8_0_5_4 (order: [7,1,6,2,3,8,0,5,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[5]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // DIV + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a / regs[base + _operands[5]]; + // BXOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a ^ regs[base + _operands[8]]; + break; + } + case 71: + { + // ANTI_LT_7_4_3_0_5_6_1_8_2 (order: [7,4,3,0,5,6,1,8,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[7]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this._constant(_operands[1], _operands[2]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // BXOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a ^ regs[base + _operands[8]]; + break; + } + case 72: + { + // ANTI_SHL_0_7_8_6_3_2_5_4_9_1 (order: [0,7,8,6,3,2,5,4,9,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[8]]; + // SHL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a << regs[base + _operands[2]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[3]; + var target = _operands[4]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // SHR + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a >> regs[base + _operands[7]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[8]; + var target = _operands[9]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 73: + { + // ANTI_BOR_2_7_1_3_6_4_0_5 (order: [2,7,1,3,6,4,0,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[1]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // UNARY_POS + var dst = _operands[3]; + regs[base + dst] = +regs[base + _operands[4]]; + // BOR + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a | regs[base + _operands[7]]; + break; + } + case 74: + { + // ANTI_BXOR_3_0_8_4_2_5_7_6_1 (order: [3,0,8,4,2,5,7,6,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[2]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // LOOSE_NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a != regs[base + _operands[5]]; + // BOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a | regs[base + _operands[8]]; + break; + } + case 75: + { + // ANTI_USHR_7_1_4_0_2_6_5_3 (order: [7,1,4,0,2,6,5,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[0]]; + // LOAD_INT + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[2]; + var name = this._constant(_operands[3], _operands[4]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // USHR + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a >>> regs[base + _operands[7]]; + break; + } + case 76: + { + // ANTI_UNARY_BITNOT_0_8_1_4_3_6_5_9_7_2 (order: [0,8,1,4,3,6,5,9,7,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[7]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // TYPEOF + var dst = _operands[2]; + regs[base + dst] = typeof regs[base + _operands[3]]; + // DIV + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a / regs[base + _operands[6]]; + // NEQ + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a !== regs[base + _operands[9]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/qualification.json new file mode 100644 index 00000000..bfcdbe5d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/qualification.json @@ -0,0 +1,78 @@ +{ + "id": "exceptions--stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodes--97", + "case": "exceptions", + "profile": "stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodes", + "seed": 97, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": true, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true, + "concealConstants": true, + "antiInstrumentation": true, + "specializedOpcodes": true, + "macroOpcodes": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/encoded.js b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/encoded.js new file mode 100644 index 00000000..671b6dc6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/encoded.js @@ -0,0 +1,2258 @@ +var CONSTANTS = [/* 0 */1283219821, /* 1 */535677878, /* 2 */"B38Xw45IarGVKUVhufg=", /* 3 */"gncJ6/4cTbWdNSOdkQxcd2QTXa9vJWvVU02K/BtHtstBv0ElZuVNfWONZwdIg20PcpNl5c9tRN1St15LFN9vSwL1XV1OLa22PxO6jz4DpZYCLbq9HiVma7z/DkuMxnI9/K1K5ZpiHO+5k8cG9ILhHcn15Goh364rAbeF2uVNqvXuUtCu6iPIl9ES6X6+BUqKij7Kq8dnuvrKTvjFlkXVTurSs2c64okeILKQBQ+e4Ap4do0bia4vOpnVDU6awhtWntMXvpEiYJYJfnKKWwYmuiQus5oL5qVtY9ItRkFCe94Dcj7mYGJwKmK2TBq4j1R6c5ZMKniieNZuUmf+PUKt1g0ytqk75uF6fA6Ym0gGCYr4EQbmsmIH3owzN0a1wg1O4fHHWoau9DvTl+QKJH66CRqSjD6lo8RX4PLiTrPBxLnTLsfa6mcE64MewomqAfyezfLWZ8Pj464MMYrZL0GS+hpXvysBv/8qiJEWefeCDBedsysvmmIX+ZNxOdlPR2lLTt9BaxbhtJkaMcyGHwNSj3hzO5kzIXO5NNFmS3j/fktw1yc5uKFBGWpzGQ8Og1M3f7FwGSrhj2g/385LZkex2AdxhPl4YTCvpjMwh/0AEnmoEXyphiA1q8xX7/j1QPTZlLG2KBrjiXdH8+AY64HoCcuQiujsZ+/btbDhOIzBX0jt8PFn4yPfuI8Q8In6AYuIBBCPuxEguVhh8LpJJ8CmSMqzKtiRYDzogpEPCMeAaBii7xF4W+AMKGewTch6QKD/JlC7qDcgVah4EHR4AABSmGowe6hjYHjon3AP2DtAL0hQ0HtoWeBbGEqQujslgLYINHCF6TWg5jg90HhI98AiSJTRLbihIBeYs3cTiKkAgYnrMdOY4ODSZ/XTnEgUQIbZQFD2+KZn4ivIsNII0oHYebeQ+xiYo0UvxFCk+eZB0dj/MOyr5d+2aFbxuZkPAam4NBOO7w1jydiIUTe5u8AoS7D/M2ugoBY5lpEfGVeDSA9Ck203I6GuWSnxjUhG3ytLeTdt2QthS/lGEXKPewM0h50SBnkj4T+pRD9Ky1NHE/tmUdXZI6G0LkWTuHd0g7MeOLFoKZqeMuubd3/bvk5FueTRPGm04u9n4hOMvtICzIkJcbvqVCfFu6Au0lr5wbBJydLXNuqj6h/Gchz2v4HUCt+2/hue74x649bPSTZCmMY8UrX/NGKuphcymIkvBmh64A9wmp8mPKqZUTL2kkJy3rWzeTZUokRuW/ZaGjiOoDtMlovqen5/5l1SYD5ywkdGQfJ+bkciMLqyLiOaaQZ6imEeBapWIk9eN/Kud0XCl05fsprVFm65+m2WNAq2vjsK8pUkfoviESbisyrO4kKWxe5Nz9r3JqmrBx63ik0G8p3SMs+2sSPT78Jy4ta3Tec959YOa57/wGr6lt49no2aBuVy8+8Vo5gmHK3HXQvFsUo63/yrziYpnaltCvW5AhuPijNIl4fiNH1H1RZVEz9jy0RXr/o9bbolQrV9D3iDTQde80YdbKUxJURdRsuQR0fbhEx4tSolXW1R81+XWwOevFIVn5UFfbfsJlekuyXP/0QL9QFtidwpJ42zBR+lhD4FlZ0bLf2k9FuX78J0+MSTTRo9kNQVY6f/9RTMlL496o3VFNZ06e/5W5w0FMzHXczE33Ty346j9iTmnLgNWIS8BCqMmDskpIjsJXyi1a9ECrzx0w1U+/wzbKMVI7ToDEacZXRn7GUcc6RtNNFcAszYRzfcXyxWpD4kbmw2/CqEVAwyvGwUheUBfHvsKVR9vEvMd1RJ9ENsk5w4NJ6zHRyihB/04514LFOkg1smzPNEK8T9TRc8lKQ6ZLf/5hT9hMk9kY3CFOlkpe9HVLc05s3YXeD0+XT13/Oj+DS0nPANJ4WlFBOvrjvYpMfs6H2VxZZE1byK0x5nmfw8bZEVBYWtDB2b/HeW7AZdjbUvNbBcRsOCdxDTmCxkpXUVdI1D+3WHPgvBvA4lxuVjfXfTY0cywwjPUVVD5SxteJ0DN4KLOh8kdVf1R51TLUK3cFNsz51CAdWqrRg9tqZra6D/dgsDhYQ9F62J5iBjgu8aU63GOc2GXcz27GrxH9Wr/DZbnZoNJPW8EtKv4iPRVsLS1H3mxc9K6r7UK0tmlPr8je8Fz4LvDvOTx2fX4iReqrVHysJePMq8dw3apC4uuikShI4uAr6XCBKMvgoil9UcfnDKUkZ8OmSuTlpD5i9upJIdBmeCbh53cj7ma15jKim2T1pLzjl6Eta3qX0iKJZnEkL+SwJylk8yFaq55h16os8BWq/GYsqCUWjmCmL3HweysgYzgoQJRPaUGheuhjvpVpHK60GLydRSBL6/IymW3PLWic8Bvbr3LuKb1Gfk6rpejMkDwaRe8fPLZ8bi3i7SscUZ8I4z+4WXCiugviUplNErefbC81c0M/2uIGGG+Rlxn5oAB4KLNx9saXThR1lPMU/Hh0Mez5lyAtl8oS05epdIC3v/LgtAkVg5a6G92D5z3c9fQ1W5bzEIWUzhS2umHz2LsQAwmctxOvmjYACvlTP+WHPB", /* 4 */-542582067, /* 5 */-1997607433, /* 6 */667671940, /* 7 */2081166017, /* 8 */-623568127, /* 9 */1004051843, /* 10 */"LVGhOdXO0lq+9ytr", /* 11 */undefined, /* 12 */187821411, /* 13 */-401520769, /* 14 */-1877819919, /* 15 */"T5BRCB2Avhk=", /* 16 */-1016167309, /* 17 */-47503341, /* 18 */-479412354, /* 19 */"5Uq73o+pzSH2nvly0fc=", /* 20 */"14X2Ncee9+LlZ+HTiU7oRQ==", /* 21 */1650705589, /* 22 */"dnxF1BVUdTxizCtUAvSxbykk", /* 23 */"B2e74zcv8rLCxdlN", /* 24 */1869576073, /* 25 */"GVmm4Tx5rZDUD+mD1xjokJ35", /* 26 */"FdzLTD805dsgbJnkjhXejQ==", /* 27 */"uykTopgW8pP4f/qC", /* 28 */-538045243, /* 29 */"3BfQe8Tu+Fr6McPJ60L0xw==", /* 30 */"D3SOjRUcYbRtNFucYeR7dA==", /* 31 */"W8V+TRPdkyRho20fcnN4BQ==", /* 32 */-1696481751, /* 33 */"WW9f5UFVI823vAtHlNMUf6/kNKU=", /* 34 */"rVDn565jtxiwsMsJ5YGtCMbw", /* 35 */-404886966, /* 36 */"uQUNhU0PeptzJ0qrct20dR7F", /* 37 */"JAuAn85o2uDDWcSx", /* 38 */"vS1nlXoDcY91E1a3VC0=", /* 39 */-1057472639, /* 40 */"ClSQ8/FsBiQ=", /* 41 */"5sXjTl7ajbc=", /* 42 */"GS2dtRAVhI4FA+2X", /* 43 */-1098768240, /* 44 */"P53YCh630xs/79F6UtUfTQ==", /* 45 */"/6vIP/FI", /* 46 */63577299, /* 47 */"mMwydLDV0Kw6PA==", /* 48 */"O25W+lGGWApkvlMqaOZoflfS", /* 49 */"aiZB6pF9C9Y=", /* 50 */722902426, /* 51 */"JmbN7XSa", /* 52 */"V65vEmmGEQ6vmiY2gasn3vBqMPY=", /* 53 */"OpWSNACv", /* 54 */-1810625817, /* 55 */"Gem2cUeZsgIah5AT", /* 56 */-958443441, /* 57 */"cllT4R55SREsj7wDPoeOEg==", /* 58 */481295401, /* 59 */"uPYzkugv", /* 60 */-460294394, /* 61 */"+Zo34f9pDNH4tphDBt+Qcg==", /* 62 */"i5A+GLqDJwg=", /* 63 */"q9kMQZT4Dlc=", /* 64 */428211854, /* 65 */"tkxH3LFFLvSuXEKsQSQ=", /* 66 */1494800154, /* 67 */"L5m5di7rrB82q502L1mVwepG1tI=", /* 68 */"gr/qQMrIzXHL6dGgxTj/j8AbxoA=", /* 69 */"LBaxig7+oosIFg==", /* 70 */"1wzNdLvlMqyEPBvU", /* 71 */"lvolDpubKDaWqibea3Zt8lBeC8I=", /* 72 */"mkwu06W32SzTnNJ1", /* 73 */"CKbPMgHe3kVOylJeQCp5plsaMe4=", /* 74 */"gklY0Ko4WqCp56Jwwwjagdc5u7A=", /* 75 */"VmV5rQo9ZId7Aw==", /* 76 */"QK9cG7R2Ng2wnSA1krIjH6fj", /* 77 */"KqvYZj35gxEFmaoC6I/KE9d2", /* 78 */"GNueTwu7QcE=", /* 79 */"oJUpDaCC1xf2k7g+MqWkVSv9kkoCx4a7", /* 80 */"Xa4bYlfmDnJQ3nNCgEU=", /* 81 */"VOtCHbm1FzWhnBADgHcDE1itWCU=", /* 82 */"v44yGrFnF+q9XjnKl8UYXg==", /* 83 */1403100827, /* 84 */"aWqs3jFK1kc=", /* 85 */"sIIhBsON", /* 86 */"", /* 87 */"+yLYt+YT", /* 88 */-1350638398, /* 89 */"JZvsLA61rwUUjb/8", /* 90 */"NC5F2lh2fco=", /* 91 */-1926223448, /* 92 */209735085, /* 93 */-1605963666, /* 94 */"jl3szKzEy1vLzw==", /* 95 */1834702298, /* 96 */"L86tWgPGYk6n0iCmtSMsnspy", /* 97 */1386915786, /* 98 */"0bsIL6jYIXH0yRFBu8g5s6JfGeM=", /* 99 */1241896880, /* 100 */"EjGOyhpfjss=", /* 101 */"Qa1G5U1lQe/Rkz8H", /* 102 */-482870849, /* 103 */"4/CaeNDB+0nlsA==", /* 104 */"Bwn1sPUb9W/J+O1Qzsk=", /* 105 */-1902452597, /* 106 */"AVjU4wBo", /* 107 */"Z49RG0WFvA0gnZjmEWOl3w==", /* 108 */-436322428, /* 109 */"k5AwF4ejKC+w0AZ5v8FQWWg3", /* 110 */"IYeOEg6ttiU=", /* 111 */1926445685, /* 112 */"V3ZOymZCnrZoMg==", /* 113 */"f86xQgO2xDEXWqbmIGo=", /* 114 */-1853496171, /* 115 */"YpNwZ3XjL22g1SxFuE4=", /* 116 */"dDxcjDoUhXsa7JtU", /* 117 */1101115424, /* 118 */"tpkMAIr4", /* 119 */-405338517, /* 120 */"rYN3FJVsffyYZQ==", /* 121 */"L/KNVhsih79hEnuWQgJOin8W", /* 122 */1055444278, /* 123 */"QhYJer8PHJq5NhCqjB015pdyEt4=", /* 124 */"+3r4Bc+N6zLNpu1j9f8=", /* 125 */-1085988809, /* 126 */"8/bRXeUtjKbgEueP8gO0hjUN", /* 127 */2049144936, /* 128 */"yOuDf4jKFUWaPQ+t", /* 129 */"EoyKBSt0pOw2nIAnP6w=", /* 130 */1383651040, /* 131 */"R9NOR1PDIEmT0SG5siB+mw==", /* 132 */687617939, /* 133 */"bP9JEY6JZQFyiy8XaHtE705Z", /* 134 */216675885, /* 135 */"G3ay8UVausbVy9Zfwer5Zg==", /* 136 */"5KwKZOX7AEyNxA5E0k3s3Ntkn+M=", /* 137 */982713495, /* 138 */"Djy0ixgUn3w=", /* 139 */"QH5rio8VGr7GIglmjvMETrDCRUY=", /* 140 */-332229734, /* 141 */"RSVGXW7LjUdr23ZNe7UxJUhtavM=", /* 142 */"OxaCuSMCeYZr8jxuiyI=", /* 143 */147936442, /* 144 */"7s7FWubF+83OUsHm+2Pun7UyIoY=", /* 145 */"GhAliCQA", /* 146 */841033412, /* 147 */"+Dv/oN1o3/E0SJrAC7g=", /* 148 */"erx6FFKUZPxdjHgUTbRVLJnkUnQ=", /* 149 */"3JG5eauC5AaJsyQu", /* 150 */"Et+hcjbJpEE=", /* 151 */"Z3pm7mVatScsqqceC4r1ASCe", /* 152 */"8/N0nKok", /* 153 */-1990493076, /* 154 */"4ZPF//iA3Qg=", /* 155 */"sg4Pktk5", /* 156 */106777436, /* 157 */"GWKVpjUygInHBvx64w8=", /* 158 */"iikdobgWb5M=", /* 159 */"e8VuwzZfOPMwZ0MddK1uNYWaCg8=", /* 160 */"lZBoCDSAJxg=", /* 161 */1843532800, /* 162 */-977554506, /* 163 */-180978573, /* 164 */721934341, /* 165 */493955707, /* 166 */-1000833098, /* 167 */"ckd7w1hPXPE=", /* 168 */-1242605443, /* 169 */-395971777, /* 170 */-130488123, /* 171 */"2KXLJchuEPqzhx8Ku74BKq7l", /* 172 */615417222, /* 173 */"SFUeJbOuARs=", /* 174 */-1250957821, /* 175 */1863604928, /* 176 */416392784, /* 177 */"dua+Kju+jBU2gsn+", /* 178 */-422211680, /* 179 */-1204217421, /* 180 */1807065043, /* 181 */1403486954, /* 182 */-91413544]; +var BYTECODE = [78, 79, 80, 47, 7, 108, 1, 79, 0, 0, 81, 42, 6, 7, 1, 8, 47, 9, 2881, 4, 95, 0, 0, 82, 111, 16, 4, 3752385228, 17, 5, 2297359861, 18, 2, 16, 17, 61, 2, 15, 13, 13, 15, 18, 15, 13, 111, 16, 6, 667671943, 17, 7, 2081166021, 18, 2, 16, 17, 61, 3, 14, 14, 14, 14, 18, 15, 14, 111, 16, 8, 3671399172, 17, 9, 1004051845, 18, 2, 16, 17, 61, 4, 13, 13, 15, 14, 18, 14, 13, 83, 84, 85, 86, 42, 5, 9, 4, 6, 10, 11, 12, 48, 17, 3, 2, 3, 4, 87, 0, 16, 11, 0, 88, 110, 73, 12, 187767967, 76, 13, 3893446527, 75, 14, 2417157874, 74, 15, 956739603, 47, 70, 6600, 3, 15, 0, 0, 89, 0, 16, 16, 3278804708, 61, 17, 73, 73, 73, 74, 16, 74, 73, 0, 18, 13, 3893446527, 61, 19, 74, 74, 74, 73, 18, 74, 74, 0, 20, 17, 4247493818, 62, 21, 73, 17, 18, 73, 74, 73, 20, 73, 73, 3815539461, 63, 73, 74, 21, 73, 71, 74, 73, 74, 74, 73, 73, 64, 74, 74, 73, 71, 73, 71, 73, 73, 74, 73, 90, 1, 72, 547180537, 65, 73, 72, 73, 19, 73, 73, 1955947324, 67, 73, 72, 73, 74, 66, 0, 73, 74, 73, 73, 73, 74, 73, 74, 72, 74, 72, 71, 74, 67, 3323211306, 20, 67, 73, 74, 73, 73, 74, 1, 72, 73, 67, 73, 105, 68, 22116, 69, 16173, 77, 0, 22, 21, 1650666957, 61, 19, 74, 73, 73, 74, 22, 73, 73, 68, 73, 73, 74, 73, 17, 74, 23, 73, 73, 73, 19, 73, 74, 63, 74, 73, 23, 74, 71, 73, 73, 74, 73, 74, 73, 64, 74, 74, 73, 71, 73, 71, 73, 74, 74, 74, 105, 67, 2609611047, 72, 2393708257, 65, 73, 72, 73, 22, 73, 73, 3481507910, 67, 73, 72, 73, 73, 66, 0, 74, 73, 73, 73, 73, 73, 73, 74, 72, 73, 72, 71, 73, 67, 2275659734, 23, 67, 73, 74, 74, 74, 73, 0, 72, 73, 67, 73, 105, 68, 48405, 69, 5349, 77, 106, 67, 1106033899, 68, 43356, 69, 8821, 77, 0, 24, 24, 1869606725, 65, 73, 25, 74, 25, 73, 73, 2927215427, 24, 74, 19, 73, 74, 61, 19, 74, 73, 74, 73, 25, 73, 74, 68, 73, 73, 73, 73, 17, 73, 26, 74, 74, 73, 19, 73, 74, 63, 73, 73, 26, 74, 71, 73, 74, 73, 74, 73, 73, 64, 74, 73, 74, 71, 73, 71, 73, 74, 73, 73, 105, 67, 2040463405, 72, 1824364007, 65, 74, 72, 74, 26, 73, 73, 3826996536, 67, 73, 72, 73, 74, 66, 1, 74, 74, 73, 73, 73, 73, 74, 73, 72, 73, 71, 72, 74, 67, 1901059093, 27, 67, 74, 74, 74, 74, 74, 2, 72, 73, 67, 73, 105, 68, 49117, 69, 42278, 77, 106, 67, 3892045564, 68, 26536, 69, 2874, 77, 0, 27, 28, 3756923938, 67, 1382291895, 29, 28, 74, 74, 73, 74, 74, 1, 27, 74, 19, 73, 61, 19, 73, 74, 74, 73, 28, 74, 74, 68, 74, 73, 74, 74, 17, 73, 29, 73, 73, 73, 19, 73, 73, 63, 74, 73, 29, 73, 71, 74, 73, 73, 74, 73, 73, 64, 74, 74, 74, 71, 73, 71, 73, 73, 73, 73, 105, 67, 2976025795, 72, 2759254653, 65, 74, 72, 74, 30, 74, 74, 2330968983, 67, 74, 72, 73, 74, 66, 2, 74, 74, 73, 74, 74, 74, 73, 73, 72, 74, 71, 72, 73, 67, 2382441246, 31, 67, 74, 73, 73, 73, 73, 0, 72, 74, 67, 73, 105, 68, 64968, 69, 20219, 77, 106, 67, 1080254731, 68, 29620, 69, 50109, 77, 0, 30, 32, 2598452623, 67, 284138584, 33, 31, 73, 73, 74, 74, 73, 0, 30, 74, 19, 74, 61, 19, 74, 73, 73, 74, 31, 74, 74, 68, 74, 73, 73, 73, 17, 73, 32, 74, 74, 73, 19, 73, 74, 63, 74, 73, 32, 74, 71, 74, 73, 73, 74, 74, 73, 64, 73, 74, 74, 71, 74, 71, 74, 73, 73, 74, 69, 73, 74, 71, 74, 73, 73, 71, 105, 67, 3865646695, 72, 3643051041, 65, 74, 72, 74, 34, 73, 73, 4136598791, 67, 74, 72, 74, 73, 70, 73, 74, 74, 74, 72, 73, 74, 74, 73, 71, 73, 73, 72, 74, 67, 1382291895, 29, 67, 73, 73, 74, 73, 73, 1, 72, 74, 67, 73, 105, 68, 48499, 69, 7278, 77, 106, 67, 830300202, 68, 43857, 69, 55942, 77, 0, 33, 35, 3890071546, 65, 74, 34, 73, 36, 73, 74, 1804849616, 33, 74, 19, 74, 74, 61, 19, 74, 73, 74, 74, 34, 74, 74, 68, 74, 74, 73, 73, 17, 74, 35, 74, 74, 74, 19, 73, 73, 63, 74, 74, 35, 73, 71, 74, 74, 74, 74, 73, 74, 64, 73, 74, 74, 71, 74, 71, 73, 74, 73, 74, 105, 67, 2609265363, 72, 2391511181, 65, 74, 72, 73, 37, 74, 74, 2894456014, 67, 73, 72, 74, 74, 66, 1, 73, 74, 74, 73, 73, 74, 74, 74, 72, 74, 72, 71, 74, 67, 3990410180, 38, 67, 73, 74, 74, 73, 73, 2, 72, 74, 67, 73, 105, 68, 14189, 69, 22489, 77, 106, 67, 4057764020, 68, 62872, 69, 2210, 77, 0, 36, 39, 3237493867, 67, 110229586, 40, 37, 73, 73, 73, 73, 73, 0, 36, 74, 19, 73, 61, 19, 74, 74, 73, 74, 37, 74, 74, 68, 73, 73, 73, 74, 17, 73, 38, 74, 73, 73, 19, 73, 73, 63, 73, 73, 38, 73, 71, 73, 73, 74, 73, 73, 74, 64, 74, 73, 73, 71, 73, 71, 73, 73, 73, 73, 69, 74, 74, 71, 74, 74, 73, 71, 105, 67, 2375685352, 72, 2160036514, 65, 73, 72, 73, 41, 74, 73, 2524702287, 67, 73, 72, 73, 73, 70, 74, 73, 73, 73, 72, 74, 73, 74, 74, 71, 73, 73, 72, 74, 67, 2156355304, 42, 67, 74, 74, 73, 73, 73, 1, 72, 73, 67, 73, 105, 68, 27236, 69, 13299, 77, 106, 67, 3020521501, 68, 55375, 69, 30503, 77, 0, 39, 43, 3196223564, 65, 73, 40, 73, 44, 74, 74, 1611658666, 39, 74, 19, 74, 73, 61, 19, 74, 73, 73, 73, 40, 74, 74, 68, 74, 74, 74, 74, 17, 73, 41, 74, 73, 74, 19, 74, 74, 63, 74, 73, 41, 73, 71, 73, 74, 73, 73, 74, 73, 64, 74, 73, 73, 71, 74, 71, 73, 74, 73, 73, 105, 67, 50371678, 72, 4127920663, 65, 74, 72, 74, 45, 73, 74, 2224243752, 67, 73, 72, 74, 73, 66, 0, 73, 74, 74, 74, 73, 73, 73, 73, 72, 73, 71, 72, 73, 67, 1382291895, 29, 67, 73, 74, 73, 73, 73, 2, 72, 73, 67, 73, 105, 68, 37291, 69, 24227, 77, 106, 67, 496429799, 68, 17562, 69, 15862, 77, 0, 42, 46, 63617976, 67, 1719299993, 47, 43, 73, 73, 73, 73, 73, 1, 42, 73, 19, 73, 61, 19, 73, 73, 73, 73, 43, 73, 73, 68, 74, 74, 73, 73, 17, 73, 44, 73, 74, 74, 19, 73, 74, 63, 73, 73, 44, 73, 71, 73, 73, 73, 74, 74, 73, 64, 74, 74, 73, 71, 74, 71, 73, 74, 73, 74, 105, 67, 3774509535, 72, 3554666393, 65, 73, 72, 73, 48, 73, 73, 1677592077, 67, 73, 72, 73, 74, 66, 1, 73, 74, 73, 73, 74, 73, 74, 74, 72, 73, 71, 72, 73, 67, 3126683856, 49, 67, 74, 73, 73, 73, 74, 1, 72, 73, 67, 73, 105, 68, 46861, 69, 10163, 77, 106, 67, 3643828020, 68, 28455, 69, 21870, 77, 0, 45, 50, 722875447, 65, 74, 46, 74, 51, 73, 74, 1674902641, 45, 74, 19, 74, 73, 61, 19, 74, 74, 73, 73, 46, 74, 74, 68, 73, 74, 74, 74, 17, 74, 47, 73, 74, 74, 19, 74, 74, 63, 73, 74, 47, 74, 71, 73, 73, 73, 73, 73, 73, 64, 73, 73, 74, 71, 74, 71, 74, 74, 74, 73, 69, 74, 73, 71, 73, 73, 74, 71, 105, 67, 1407934642, 72, 1187894892, 65, 74, 72, 74, 52, 73, 73, 773804757, 67, 74, 72, 74, 73, 70, 73, 74, 74, 74, 72, 74, 74, 74, 74, 71, 73, 74, 72, 73, 67, 4138449076, 53, 67, 73, 73, 73, 73, 73, 0, 72, 73, 67, 74, 105, 68, 20099, 69, 7340, 77, 106, 67, 3375218651, 68, 17041, 69, 26018, 77, 0, 48, 54, 2484360524, 67, 1719299993, 47, 49, 74, 74, 74, 74, 74, 0, 48, 74, 19, 74, 61, 19, 74, 73, 74, 74, 49, 74, 74, 68, 74, 74, 74, 73, 17, 74, 50, 74, 74, 74, 19, 73, 73, 63, 73, 74, 50, 73, 71, 73, 74, 73, 73, 74, 73, 64, 74, 74, 73, 71, 74, 71, 74, 74, 73, 74, 105, 67, 1731588414, 72, 1510172408, 65, 73, 72, 74, 55, 73, 74, 738475772, 67, 74, 72, 73, 74, 66, 2, 74, 73, 73, 74, 74, 74, 74, 74, 72, 73, 72, 71, 73, 67, 1719299993, 47, 67, 74, 73, 74, 74, 73, 1, 72, 74, 67, 73, 105, 68, 41996, 69, 25168, 77, 106, 67, 3863280784, 68, 59206, 69, 20788, 77, 0, 51, 56, 3336507074, 67, 1719299993, 47, 52, 74, 74, 74, 74, 73, 2, 51, 74, 19, 74, 61, 19, 74, 74, 74, 73, 52, 73, 74, 68, 74, 74, 74, 74, 17, 73, 53, 74, 74, 73, 19, 74, 73, 63, 73, 74, 53, 73, 71, 73, 74, 74, 74, 74, 73, 64, 74, 74, 73, 71, 73, 71, 74, 74, 74, 74, 105, 67, 4091533442, 72, 3875122748, 65, 74, 72, 74, 57, 74, 74, 1988592412, 67, 73, 72, 74, 74, 66, 0, 74, 73, 74, 74, 73, 74, 74, 74, 72, 74, 72, 71, 73, 67, 2382441246, 31, 67, 74, 74, 73, 74, 74, 0, 72, 74, 67, 73, 105, 68, 56819, 69, 19372, 77, 106, 67, 2509504897, 68, 14299, 69, 35897, 77, 0, 54, 58, 481289172, 67, 3323211306, 20, 55, 74, 73, 73, 73, 74, 0, 54, 74, 19, 74, 61, 19, 73, 74, 73, 74, 55, 73, 73, 68, 73, 73, 73, 73, 17, 74, 56, 74, 73, 74, 19, 73, 73, 63, 74, 74, 56, 74, 71, 74, 73, 74, 74, 73, 73, 64, 74, 73, 73, 71, 74, 71, 74, 74, 73, 73, 105, 67, 1597865179, 72, 1373565589, 65, 74, 72, 74, 59, 73, 74, 1891305388, 67, 73, 72, 73, 73, 66, 0, 73, 73, 73, 73, 73, 74, 73, 73, 72, 73, 72, 71, 74, 67, 3323211306, 20, 67, 73, 74, 73, 73, 73, 2, 72, 74, 67, 73, 105, 68, 19872, 69, 35415, 77, 106, 67, 337268243, 68, 6505, 69, 54842, 77, 0, 57, 60, 3834647084, 65, 74, 58, 73, 61, 74, 74, 3792379676, 57, 73, 19, 73, 73, 61, 19, 73, 74, 74, 74, 58, 73, 73, 68, 73, 73, 74, 73, 17, 73, 59, 73, 73, 74, 19, 73, 73, 63, 74, 73, 59, 73, 71, 73, 74, 74, 74, 74, 74, 64, 73, 73, 74, 71, 74, 71, 73, 74, 73, 74, 105, 67, 1338640811, 72, 1122934629, 65, 74, 72, 74, 62, 74, 74, 563475689, 67, 74, 72, 73, 74, 66, 0, 74, 74, 73, 74, 74, 73, 73, 73, 72, 74, 72, 71, 73, 67, 11777286, 63, 67, 74, 74, 74, 74, 74, 0, 72, 73, 67, 74, 105, 68, 11424, 69, 38035, 77, 106, 67, 2200119521, 68, 50311, 69, 47621, 77, 0, 60, 64, 428207542, 67, 4138449076, 53, 61, 74, 74, 74, 73, 73, 1, 60, 73, 19, 74, 61, 19, 74, 74, 74, 74, 61, 74, 73, 68, 73, 74, 74, 74, 17, 74, 62, 74, 74, 73, 19, 74, 74, 63, 73, 73, 62, 73, 71, 73, 73, 74, 74, 73, 74, 64, 73, 73, 74, 71, 74, 71, 73, 73, 73, 73, 69, 74, 73, 71, 74, 74, 74, 71, 105, 67, 540520602, 72, 318842452, 65, 74, 72, 73, 65, 74, 73, 2444671465, 67, 73, 72, 74, 73, 70, 74, 74, 73, 74, 72, 74, 73, 74, 74, 71, 74, 74, 72, 74, 67, 2156355304, 42, 67, 73, 74, 73, 74, 73, 1, 72, 73, 67, 74, 105, 68, 42047, 69, 51981, 77, 106, 67, 2888292709, 68, 14364, 69, 37532, 77, 0, 63, 66, 1494808208, 65, 74, 64, 73, 67, 74, 73, 3946207348, 63, 73, 19, 74, 74, 61, 19, 73, 73, 74, 74, 64, 74, 73, 68, 73, 74, 74, 74, 17, 73, 65, 73, 73, 74, 19, 74, 73, 63, 74, 74, 65, 73, 71, 74, 74, 74, 73, 73, 73, 64, 73, 74, 74, 71, 74, 71, 74, 73, 73, 74, 105, 67, 1467502099, 72, 1247601613, 65, 74, 72, 73, 68, 73, 74, 1245545099, 67, 74, 72, 73, 73, 66, 1, 73, 74, 74, 73, 74, 74, 73, 73, 72, 74, 72, 71, 73, 67, 92115508, 69, 67, 73, 73, 73, 73, 74, 1, 72, 73, 67, 74, 105, 68, 1650, 69, 42276, 77, 106, 67, 75041867, 68, 9173, 69, 29840, 77, 106, 67, 820807271, 68, 45671, 69, 3387, 77, 0, 77, 70, 1643549714, 91, 71, 74, 74, 73, 74, 74, 74, 74, 2, 77, 5, 74, 78, 74, 74, 74, 63, 73, 73, 77, 73, 71, 74, 73, 74, 74, 73, 74, 64, 74, 73, 73, 71, 74, 71, 73, 74, 74, 73, 105, 67, 2441284771, 72, 3097734005, 65, 74, 72, 73, 71, 73, 73, 3933685225, 67, 74, 72, 73, 73, 66, 2, 74, 74, 73, 73, 73, 74, 74, 73, 72, 74, 71, 72, 74, 67, 1686888730, 72, 67, 74, 73, 73, 74, 74, 0, 72, 73, 67, 74, 105, 68, 54770, 69, 6050, 77, 1, 66, 16116, 65, 74, 17, 74, 73, 74, 74, 1273134340, 66, 74, 17, 74, 73, 106, 67, 1217332982, 68, 45332, 69, 48535, 77, 1, 66, 1679, 65, 74, 17, 74, 74, 74, 74, 2347736867, 66, 74, 17, 74, 74, 106, 67, 3093597972, 68, 49250, 69, 43439, 77, 67, 3120638745, 75, 77, 74, 73, 73, 73, 73, 0, 9, 74, 4, 73, 61, 4, 74, 74, 73, 73, 77, 74, 73, 1, 66, 32394, 67, 379555796, 76, 17, 73, 73, 74, 73, 74, 0, 66, 73, 17, 73, 106, 67, 554561410, 68, 61844, 69, 26237, 77, 1, 66, 57501, 71, 73, 73, 74, 74, 74, 74, 74, 2, 66, 17, 73, 66, 73, 74, 73, 63, 73, 73, 66, 73, 71, 74, 74, 73, 74, 74, 73, 64, 74, 73, 73, 71, 74, 71, 73, 74, 73, 73, 105, 67, 3047848429, 72, 2658259464, 65, 73, 72, 73, 77, 73, 73, 1058522985, 67, 74, 72, 73, 73, 66, 0, 74, 74, 74, 73, 74, 74, 73, 74, 72, 74, 72, 71, 74, 67, 1901059093, 27, 67, 74, 74, 74, 74, 74, 2, 72, 74, 67, 74, 105, 68, 8341, 69, 58175, 77, 1, 66, 8020, 67, 4138449076, 53, 17, 74, 74, 73, 74, 74, 0, 66, 73, 17, 74, 106, 67, 3594334381, 68, 30336, 69, 27975, 77, 1, 66, 40414, 67, 110229586, 40, 17, 73, 74, 73, 74, 73, 1, 66, 73, 17, 73, 106, 67, 2735043297, 68, 59410, 69, 60513, 77, 92, 93, 42, 6, 77, 1, 0, 1, 66, 18172, 67, 2156355304, 42, 17, 73, 73, 74, 73, 73, 1, 66, 74, 17, 73, 106, 67, 3598722672, 68, 29099, 69, 62836, 77, 94, 95, 0, 77, 79, 83563161, 96, 1, 66, 10694, 67, 3120638745, 75, 17, 74, 73, 73, 74, 73, 1, 66, 74, 17, 73, 106, 67, 2135572086, 68, 41595, 69, 43261, 77, 1, 66, 55587, 65, 73, 17, 74, 80, 73, 74, 4219464172, 66, 73, 17, 74, 74, 106, 67, 4188046335, 68, 40472, 69, 9884, 77, 0, 77, 81, 1263086969, 97, 109, 13, 4, 3752385228, 66, 4849, 65, 74, 17, 74, 82, 74, 74, 3556681151, 66, 74, 17, 74, 73, 106, 67, 864170725, 68, 13052, 69, 18994, 77, 72, 2608, 77, 14, 74, 12, 74, 75, 73, 73, 10, 74, 73, 77, 74, 78, 73, 73, 74, 73, 43, 9, 7, 8, 1, 78, 1, 66, 24491, 67, 1719299993, 47, 17, 73, 73, 74, 74, 73, 0, 66, 73, 17, 74, 106, 67, 2419295162, 68, 63012, 69, 18789, 77, 67, 1382291895, 29, 77, 74, 74, 74, 74, 73, 0, 13, 73, 5, 74, 43, 12, 3, 11, 1, 77, 109, 14, 83, 1403100819, 66, 35907, 65, 74, 17, 74, 84, 74, 74, 377771532, 66, 74, 17, 74, 73, 106, 67, 3449867224, 68, 55960, 69, 20542, 77, 0, 77, 11, 0, 98, 0, 77, 13, 3893446527, 61, 5, 73, 73, 73, 74, 77, 73, 73, 1, 66, 12450, 67, 3323211306, 20, 17, 73, 73, 74, 73, 74, 2, 66, 74, 17, 73, 106, 67, 3905141999, 68, 496, 69, 20903, 77, 0, 77, 5, 2297359861, 67, 11777286, 63, 78, 73, 73, 74, 74, 73, 2, 77, 73, 5, 73, 61, 5, 74, 74, 73, 74, 78, 74, 73, 1, 66, 13448, 65, 74, 17, 73, 85, 73, 73, 2040447630, 66, 73, 17, 74, 73, 106, 67, 3831442670, 68, 17972, 69, 60381, 77, 61, 3, 73, 73, 73, 73, 6, 73, 73, 0, 77, 86, 449554457, 61, 4, 73, 73, 73, 73, 77, 73, 73, 1, 66, 12237, 67, 2275659734, 23, 17, 73, 74, 73, 74, 73, 0, 66, 73, 17, 73, 106, 67, 301791754, 68, 26052, 69, 6219, 77, 0, 77, 81, 1263086969, 99, 43, 10, 3, 78, 1, 5, 1, 66, 11696, 65, 73, 17, 73, 87, 74, 73, 3630833124, 66, 74, 17, 74, 73, 106, 67, 3812173600, 68, 9880, 69, 1553, 77, 42, 67, 70, 3, 67, 68, 69, 100, 108, 91, 88, 2944309451, 90, 89, 1505143424, 92, 90, 721808942, 47, 87, 7009, 3, 16, 0, 0, 101, 0, 21, 91, 2368793511, 61, 22, 90, 90, 90, 90, 21, 92, 90, 0, 23, 13, 3893446527, 61, 24, 92, 92, 91, 92, 23, 91, 90, 0, 25, 92, 209741828, 62, 26, 90, 22, 93, 90, 90, 90, 25, 90, 90, 2688962025, 63, 90, 91, 26, 90, 88, 91, 90, 91, 90, 90, 92, 64, 92, 90, 91, 88, 90, 88, 91, 91, 90, 92, 105, 84, 3367496787, 89, 3642943965, 65, 91, 89, 91, 94, 91, 90, 3441134954, 84, 92, 89, 90, 91, 66, 0, 90, 90, 91, 91, 91, 92, 91, 92, 89, 90, 89, 88, 90, 67, 2382441246, 31, 84, 90, 90, 90, 91, 91, 1, 89, 90, 84, 91, 105, 85, 61554, 86, 1133, 76, 0, 27, 95, 1834683749, 61, 24, 91, 91, 91, 91, 27, 92, 91, 68, 92, 90, 91, 90, 22, 90, 28, 91, 90, 90, 24, 91, 90, 63, 91, 90, 28, 92, 88, 91, 90, 92, 92, 92, 92, 64, 92, 92, 91, 88, 92, 88, 92, 92, 92, 90, 69, 90, 91, 88, 92, 91, 91, 88, 105, 84, 2741879055, 89, 3544551615, 65, 90, 89, 91, 96, 90, 90, 161117973, 84, 90, 89, 90, 90, 70, 90, 90, 91, 90, 89, 91, 90, 90, 92, 88, 90, 92, 89, 90, 67, 110229586, 40, 84, 92, 92, 90, 91, 90, 0, 89, 91, 84, 92, 105, 85, 63576, 86, 51079, 76, 106, 84, 2803380062, 85, 33173, 86, 53777, 76, 0, 29, 97, 1386921722, 67, 4138449076, 53, 30, 91, 91, 90, 92, 91, 1, 29, 90, 24, 91, 61, 24, 91, 91, 90, 91, 30, 91, 91, 68, 90, 91, 90, 91, 22, 91, 31, 90, 91, 91, 24, 92, 90, 63, 90, 92, 31, 91, 88, 90, 91, 91, 90, 91, 91, 64, 92, 90, 92, 88, 92, 88, 91, 92, 92, 91, 69, 92, 92, 88, 90, 90, 90, 88, 105, 84, 2743076630, 89, 3549976326, 65, 91, 89, 92, 98, 91, 92, 3969414157, 84, 91, 89, 92, 90, 70, 92, 92, 91, 92, 88, 90, 91, 92, 90, 89, 92, 90, 89, 92, 67, 11777286, 63, 84, 92, 92, 91, 91, 90, 1, 89, 91, 84, 90, 105, 85, 36988, 86, 59075, 76, 106, 84, 3322083324, 85, 55708, 86, 4706, 76, 0, 32, 99, 1241894180, 65, 91, 33, 91, 100, 92, 91, 726533054, 32, 91, 24, 91, 92, 61, 24, 91, 90, 91, 91, 33, 90, 92, 68, 91, 91, 92, 90, 22, 91, 34, 91, 90, 91, 24, 91, 92, 63, 92, 91, 34, 91, 88, 91, 90, 91, 91, 91, 91, 64, 90, 91, 91, 88, 91, 88, 91, 91, 91, 91, 69, 92, 91, 88, 91, 91, 90, 88, 105, 84, 213752221, 89, 1016457677, 65, 91, 89, 92, 101, 90, 90, 3883796194, 84, 91, 89, 91, 90, 70, 91, 92, 91, 92, 89, 92, 91, 92, 92, 88, 92, 92, 89, 90, 67, 1901059093, 27, 84, 92, 92, 91, 92, 92, 1, 89, 91, 84, 92, 105, 85, 62582, 86, 34068, 76, 106, 84, 3334103629, 85, 26117, 86, 7092, 76, 0, 35, 102, 3812077898, 65, 90, 36, 91, 103, 90, 90, 264775074, 35, 91, 24, 90, 91, 61, 24, 92, 92, 92, 91, 36, 92, 91, 68, 90, 90, 91, 90, 22, 91, 37, 91, 90, 91, 24, 91, 90, 63, 91, 92, 37, 92, 88, 92, 91, 91, 92, 91, 91, 64, 90, 92, 92, 88, 91, 88, 92, 90, 90, 90, 105, 84, 3584792887, 89, 92530983, 65, 91, 89, 91, 104, 91, 91, 102084935, 84, 90, 89, 91, 92, 66, 2, 92, 92, 92, 90, 90, 90, 90, 90, 89, 92, 88, 89, 91, 67, 1719299993, 47, 84, 90, 91, 91, 91, 91, 1, 89, 91, 84, 91, 105, 85, 64857, 86, 47521, 76, 106, 84, 1101454199, 85, 28177, 86, 7951, 76, 0, 38, 105, 2392478498, 67, 110229586, 40, 39, 90, 90, 91, 91, 91, 1, 38, 91, 24, 91, 61, 24, 90, 91, 90, 90, 39, 91, 92, 68, 91, 92, 91, 90, 22, 91, 40, 90, 91, 90, 24, 90, 91, 63, 90, 91, 40, 91, 88, 91, 91, 90, 91, 90, 90, 64, 92, 91, 92, 88, 91, 88, 90, 90, 91, 91, 69, 91, 91, 88, 91, 92, 91, 88, 105, 84, 1850810773, 89, 2653483477, 65, 92, 89, 92, 106, 92, 92, 2245197432, 84, 92, 89, 92, 92, 70, 91, 91, 91, 91, 88, 90, 92, 90, 92, 89, 92, 92, 89, 92, 67, 3650533271, 107, 84, 92, 92, 90, 90, 91, 2, 89, 92, 84, 90, 105, 85, 54643, 86, 14986, 76, 106, 84, 3744976794, 85, 256, 86, 65080, 76, 0, 41, 108, 3858655467, 65, 92, 42, 91, 109, 92, 92, 799507573, 41, 92, 24, 92, 91, 61, 24, 92, 90, 90, 91, 42, 92, 92, 68, 92, 91, 90, 92, 22, 91, 43, 92, 92, 92, 24, 91, 90, 63, 92, 91, 43, 91, 88, 91, 91, 90, 91, 91, 92, 64, 91, 90, 90, 88, 91, 88, 91, 90, 92, 91, 69, 91, 91, 88, 91, 92, 91, 88, 105, 84, 538323912, 89, 1340963752, 65, 92, 89, 90, 110, 90, 92, 3505161410, 84, 90, 89, 91, 91, 70, 92, 92, 90, 91, 89, 91, 92, 91, 90, 88, 90, 91, 89, 92, 67, 3126683856, 49, 84, 91, 91, 92, 91, 90, 0, 89, 92, 84, 91, 105, 85, 50227, 86, 38420, 76, 106, 84, 64562768, 85, 32071, 86, 34395, 76, 0, 44, 111, 1926436534, 65, 90, 45, 92, 112, 92, 90, 3742900885, 44, 90, 24, 90, 92, 61, 24, 90, 92, 90, 90, 45, 92, 92, 68, 91, 90, 90, 90, 22, 90, 46, 92, 92, 90, 24, 92, 90, 63, 92, 91, 46, 92, 88, 91, 90, 92, 92, 90, 92, 64, 91, 91, 92, 88, 92, 88, 91, 92, 92, 91, 69, 91, 92, 88, 90, 92, 91, 88, 105, 84, 18580978, 89, 821220818, 65, 91, 89, 91, 113, 91, 92, 3978763002, 84, 92, 89, 91, 90, 70, 90, 90, 92, 90, 89, 91, 90, 92, 91, 88, 91, 91, 89, 91, 67, 284138584, 33, 84, 92, 91, 92, 91, 90, 0, 89, 91, 84, 90, 105, 85, 16472, 86, 56482, 76, 106, 84, 149660131, 85, 37109, 86, 62199, 76, 0, 47, 114, 2441472534, 65, 90, 48, 91, 115, 90, 90, 190735835, 47, 90, 24, 90, 90, 61, 24, 90, 90, 92, 91, 48, 92, 90, 68, 90, 90, 90, 92, 22, 90, 49, 91, 90, 90, 24, 91, 91, 63, 91, 90, 49, 92, 88, 90, 92, 91, 92, 91, 91, 64, 91, 90, 91, 88, 91, 88, 91, 91, 90, 90, 105, 84, 3422615375, 89, 4225255263, 65, 90, 89, 92, 116, 90, 91, 3316033811, 84, 91, 89, 91, 92, 66, 0, 91, 91, 91, 90, 92, 92, 91, 91, 89, 91, 89, 88, 91, 67, 4138449076, 53, 84, 90, 90, 91, 91, 91, 2, 89, 92, 84, 91, 105, 85, 63350, 86, 65331, 76, 106, 84, 2561950137, 85, 49875, 86, 36804, 76, 0, 50, 117, 1101134538, 67, 11777286, 63, 51, 90, 90, 90, 92, 92, 0, 50, 90, 24, 90, 61, 24, 90, 90, 90, 91, 51, 90, 92, 68, 90, 90, 92, 90, 22, 90, 52, 90, 91, 92, 24, 91, 91, 63, 90, 90, 52, 91, 88, 92, 92, 90, 90, 92, 92, 64, 91, 92, 92, 88, 91, 88, 90, 90, 90, 90, 105, 84, 1389085179, 89, 2191757883, 65, 91, 89, 91, 118, 90, 91, 1549914989, 84, 90, 89, 91, 90, 66, 0, 92, 92, 91, 90, 92, 91, 91, 90, 89, 92, 88, 89, 91, 67, 110229586, 40, 84, 91, 90, 91, 91, 92, 0, 89, 90, 84, 90, 105, 85, 15636, 86, 20959, 76, 106, 84, 2100623547, 85, 57748, 86, 24667, 76, 0, 53, 119, 3889650843, 65, 90, 54, 91, 120, 91, 90, 4115572285, 53, 91, 24, 92, 91, 61, 24, 90, 90, 90, 90, 54, 91, 91, 68, 91, 91, 91, 91, 22, 92, 55, 90, 92, 90, 24, 92, 90, 63, 91, 91, 55, 92, 88, 90, 90, 91, 92, 92, 90, 64, 90, 92, 90, 88, 90, 88, 90, 90, 92, 90, 105, 84, 1979796001, 89, 2786695761, 65, 90, 89, 92, 121, 90, 92, 3837815840, 84, 90, 89, 90, 90, 66, 2, 92, 90, 90, 91, 90, 91, 91, 90, 89, 92, 88, 89, 90, 67, 92115508, 69, 84, 90, 90, 90, 92, 90, 0, 89, 92, 84, 90, 105, 85, 15727, 86, 7204, 76, 106, 84, 1535859756, 85, 63599, 86, 539, 76, 0, 56, 122, 1055441267, 65, 91, 57, 91, 123, 91, 92, 3028158920, 56, 92, 24, 92, 91, 61, 24, 90, 92, 92, 90, 57, 90, 91, 68, 91, 92, 91, 90, 22, 90, 58, 91, 91, 92, 24, 91, 91, 63, 90, 90, 58, 92, 88, 92, 90, 91, 91, 91, 90, 64, 91, 91, 90, 88, 90, 88, 91, 91, 90, 91, 105, 84, 1835593122, 89, 2638298514, 65, 91, 89, 90, 124, 92, 92, 2983231734, 84, 92, 89, 92, 92, 66, 2, 90, 92, 92, 90, 90, 91, 90, 91, 89, 92, 89, 88, 92, 67, 3650533271, 107, 84, 91, 92, 91, 92, 92, 0, 89, 90, 84, 92, 105, 85, 49263, 86, 7283, 76, 106, 84, 2451429005, 85, 16780, 86, 36050, 76, 0, 59, 125, 3208997873, 67, 11777286, 63, 60, 91, 90, 90, 91, 90, 1, 59, 90, 24, 91, 61, 24, 91, 92, 91, 91, 60, 90, 92, 68, 92, 91, 90, 91, 22, 92, 61, 90, 91, 90, 24, 92, 90, 63, 90, 90, 61, 90, 88, 90, 90, 90, 92, 91, 92, 64, 92, 90, 91, 88, 90, 88, 90, 90, 90, 90, 105, 84, 2672418012, 89, 3475090556, 65, 90, 89, 90, 126, 90, 91, 2220845171, 84, 90, 89, 90, 91, 66, 1, 92, 92, 90, 90, 91, 92, 92, 92, 89, 90, 89, 88, 91, 67, 1719299993, 47, 84, 90, 92, 92, 90, 90, 2, 89, 90, 84, 91, 105, 85, 57362, 86, 55816, 76, 106, 84, 4061285413, 85, 59107, 86, 62157, 76, 0, 62, 127, 2049123336, 65, 90, 63, 92, 128, 91, 90, 2288084420, 62, 92, 24, 90, 90, 61, 24, 90, 90, 91, 90, 63, 90, 92, 68, 91, 90, 90, 91, 22, 92, 64, 90, 92, 92, 24, 92, 90, 63, 91, 90, 64, 92, 88, 90, 90, 91, 91, 92, 92, 64, 91, 90, 90, 88, 92, 88, 92, 90, 90, 90, 69, 92, 91, 88, 92, 92, 92, 88, 105, 84, 211878742, 89, 1018778438, 65, 92, 89, 92, 129, 91, 90, 3303873462, 84, 91, 89, 92, 91, 70, 90, 92, 90, 90, 88, 90, 91, 91, 90, 89, 90, 91, 89, 91, 67, 379555796, 76, 84, 90, 90, 90, 91, 91, 0, 89, 92, 84, 91, 105, 85, 37824, 86, 29700, 76, 106, 84, 1693248862, 85, 19729, 86, 38900, 76, 0, 65, 130, 1383638685, 67, 3126683856, 49, 66, 91, 91, 91, 91, 91, 0, 65, 90, 24, 91, 61, 24, 92, 92, 91, 91, 66, 91, 91, 68, 92, 92, 90, 91, 22, 91, 67, 90, 92, 91, 24, 92, 92, 63, 92, 92, 67, 91, 88, 91, 91, 92, 91, 92, 92, 64, 90, 92, 91, 88, 91, 88, 91, 90, 92, 91, 105, 84, 518352423, 89, 1321057735, 65, 90, 89, 92, 131, 91, 90, 2575037659, 84, 91, 89, 90, 90, 66, 1, 90, 92, 90, 92, 91, 91, 92, 90, 89, 91, 89, 88, 91, 67, 3323211306, 20, 84, 91, 91, 91, 90, 91, 0, 89, 91, 84, 92, 105, 85, 14276, 86, 20854, 76, 106, 84, 2100318445, 85, 42607, 86, 13795, 76, 0, 68, 132, 687633140, 67, 3650533271, 107, 69, 91, 91, 90, 90, 91, 1, 68, 91, 24, 91, 61, 24, 90, 91, 91, 90, 69, 91, 90, 68, 91, 91, 91, 91, 22, 91, 70, 91, 92, 91, 24, 91, 90, 63, 91, 90, 70, 92, 88, 92, 90, 90, 92, 91, 91, 64, 90, 91, 90, 88, 90, 88, 90, 90, 90, 90, 105, 84, 3638109136, 89, 145814400, 65, 90, 89, 90, 133, 90, 92, 457816403, 84, 90, 89, 90, 91, 66, 2, 90, 92, 90, 90, 91, 90, 90, 92, 89, 90, 88, 89, 92, 67, 3120638745, 75, 84, 90, 91, 92, 91, 91, 2, 89, 90, 84, 92, 105, 85, 36496, 86, 35064, 76, 106, 84, 3220019777, 85, 8674, 86, 52045, 76, 0, 71, 134, 216694381, 65, 92, 72, 91, 135, 90, 92, 395861592, 71, 92, 24, 92, 91, 61, 24, 90, 91, 92, 90, 72, 92, 92, 68, 92, 90, 92, 90, 22, 91, 73, 92, 91, 91, 24, 91, 90, 63, 90, 92, 73, 92, 88, 90, 92, 92, 92, 92, 91, 64, 92, 92, 90, 88, 92, 88, 92, 91, 90, 92, 105, 84, 1174730932, 89, 1977239652, 65, 91, 89, 92, 136, 92, 92, 1256221366, 84, 91, 89, 92, 91, 66, 2, 91, 92, 91, 92, 91, 92, 92, 92, 89, 92, 89, 88, 92, 67, 2156355304, 42, 84, 92, 92, 92, 90, 92, 1, 89, 90, 84, 92, 105, 85, 25135, 86, 6470, 76, 106, 84, 4155396505, 85, 17923, 86, 39507, 76, 0, 74, 137, 982728380, 65, 91, 75, 91, 138, 91, 91, 1972709479, 74, 92, 24, 91, 92, 61, 24, 92, 90, 92, 92, 75, 92, 90, 68, 90, 90, 92, 92, 22, 90, 76, 92, 92, 90, 24, 91, 92, 63, 91, 92, 76, 91, 88, 90, 91, 91, 92, 91, 90, 64, 90, 91, 91, 88, 91, 88, 91, 91, 91, 91, 69, 91, 91, 88, 91, 91, 92, 88, 105, 84, 1071471822, 89, 1874144382, 65, 90, 89, 90, 139, 91, 91, 2312946897, 84, 90, 89, 92, 91, 70, 90, 90, 91, 91, 88, 91, 90, 92, 91, 89, 91, 90, 89, 91, 67, 11777286, 63, 84, 91, 92, 91, 91, 92, 1, 89, 91, 84, 92, 105, 85, 22677, 86, 9982, 76, 106, 84, 3801074545, 85, 49998, 86, 48628, 76, 0, 77, 140, 3962748013, 65, 91, 78, 91, 141, 92, 91, 3805393826, 77, 92, 24, 91, 91, 61, 24, 91, 91, 91, 90, 78, 92, 91, 68, 90, 91, 90, 92, 22, 91, 79, 92, 90, 91, 24, 91, 90, 63, 90, 92, 79, 90, 88, 91, 90, 91, 90, 90, 91, 64, 92, 91, 92, 88, 90, 88, 90, 90, 90, 90, 69, 91, 90, 88, 91, 90, 90, 88, 105, 84, 4062221448, 89, 569959544, 65, 91, 89, 90, 142, 90, 92, 2823465109, 84, 91, 89, 92, 90, 70, 92, 90, 90, 92, 89, 91, 90, 90, 90, 88, 90, 90, 89, 90, 67, 3990410180, 38, 84, 90, 92, 90, 91, 90, 0, 89, 91, 84, 91, 105, 85, 6410, 86, 39434, 76, 106, 84, 1341967579, 85, 18239, 86, 52988, 76, 0, 80, 143, 147933637, 67, 379555796, 76, 81, 90, 90, 90, 90, 90, 2, 80, 90, 24, 90, 61, 24, 92, 92, 90, 92, 81, 90, 90, 68, 90, 90, 90, 90, 22, 90, 82, 90, 90, 90, 24, 92, 90, 63, 92, 90, 82, 91, 88, 90, 91, 90, 92, 90, 91, 64, 91, 91, 92, 88, 91, 88, 91, 90, 91, 90, 105, 84, 142951386, 89, 949818234, 65, 90, 89, 91, 144, 92, 90, 362141496, 84, 91, 89, 90, 92, 66, 1, 91, 92, 90, 90, 92, 92, 92, 90, 89, 90, 89, 88, 92, 67, 2382441246, 31, 84, 91, 90, 92, 92, 92, 0, 89, 90, 84, 92, 105, 85, 30357, 86, 29979, 76, 106, 84, 1248737735, 85, 43723, 86, 30455, 76, 106, 84, 3328023888, 85, 22268, 86, 53230, 76, 8, 93, 0, 16, 67, 1719299993, 47, 94, 90, 90, 92, 92, 90, 0, 7, 91, 2, 90, 43, 17, 0, 93, 1, 94, 1, 83, 29773, 65, 92, 22, 92, 145, 90, 91, 3637390989, 83, 92, 22, 92, 90, 106, 84, 3469446758, 85, 26086, 86, 51072, 76, 0, 93, 11, 0, 102, 1, 83, 22417, 67, 3126683856, 49, 22, 92, 92, 90, 90, 92, 2, 83, 92, 22, 92, 106, 84, 1355836082, 85, 35226, 86, 17787, 76, 74, 90, 91, 1, 91, 91, 91, 92, 90, 92, 91, 93, 11, 75, 91, 90, 90, 93, 90, 1, 41454, 91, 12, 90, 91, 90, 109, 13, 146, 841079099, 83, 8557, 67, 3650533271, 107, 22, 90, 90, 92, 91, 91, 1, 83, 91, 22, 90, 106, 84, 2663189302, 85, 59670, 86, 63374, 76, 61, 93, 92, 92, 92, 90, 7, 90, 90, 0, 93, 4, 3752385228, 67, 1382291895, 29, 19, 92, 90, 91, 92, 90, 0, 93, 90, 7, 90, 1, 83, 3715, 65, 90, 22, 90, 147, 92, 91, 2617929728, 83, 92, 22, 90, 92, 106, 84, 320838028, 85, 23232, 86, 33580, 76, 61, 7, 90, 90, 90, 92, 19, 92, 90, 1, 83, 35071, 67, 3990410180, 38, 22, 92, 91, 90, 91, 91, 1, 83, 92, 22, 91, 106, 84, 2887435984, 85, 11644, 86, 59953, 76, 1, 83, 50327, 71, 90, 90, 92, 90, 92, 92, 91, 2, 83, 22, 92, 83, 90, 91, 90, 63, 91, 90, 83, 90, 88, 92, 90, 91, 92, 92, 92, 64, 92, 91, 91, 88, 92, 88, 91, 92, 92, 92, 69, 91, 91, 88, 92, 91, 91, 88, 105, 84, 4051055502, 89, 1222794704, 65, 91, 89, 91, 148, 92, 92, 931108854, 84, 90, 89, 90, 92, 70, 91, 92, 90, 92, 89, 90, 91, 92, 92, 88, 92, 90, 89, 90, 67, 3323211306, 20, 84, 92, 91, 90, 92, 92, 2, 89, 90, 84, 90, 105, 85, 65529, 86, 2296, 76, 1, 83, 22916, 67, 1686888730, 72, 22, 90, 92, 91, 90, 90, 1, 83, 92, 22, 92, 106, 84, 1723053755, 85, 15761, 86, 12184, 76, 1, 83, 12155, 65, 92, 22, 91, 149, 92, 92, 3427027105, 83, 91, 22, 92, 92, 106, 84, 3896905541, 85, 58631, 86, 44549, 76, 1, 83, 36899, 71, 92, 92, 91, 90, 91, 91, 90, 1, 83, 22, 91, 83, 91, 91, 92, 63, 91, 90, 83, 90, 88, 92, 92, 92, 91, 90, 92, 64, 90, 91, 92, 88, 90, 88, 90, 91, 90, 90, 105, 84, 2039271593, 89, 3603593837, 65, 91, 89, 90, 150, 91, 91, 1901209949, 84, 91, 89, 91, 91, 66, 0, 90, 91, 91, 91, 92, 90, 90, 92, 89, 92, 89, 88, 90, 67, 2382441246, 31, 84, 92, 91, 90, 90, 92, 0, 89, 92, 84, 91, 105, 85, 33213, 86, 57421, 76, 1, 83, 2130, 67, 2275659734, 23, 22, 90, 90, 92, 92, 92, 2, 83, 92, 22, 90, 106, 84, 790381173, 85, 45527, 86, 26415, 76, 1, 83, 22383, 67, 1382291895, 29, 22, 90, 90, 92, 90, 90, 1, 83, 91, 22, 91, 106, 84, 2582018611, 85, 31257, 86, 64613, 76, 70, 92, 91, 92, 91, 13, 91, 91, 91, 91, 12, 91, 91, 93, 92, 61, 8, 91, 90, 91, 91, 93, 92, 92, 103, 1, 83, 6334, 67, 1719299993, 47, 22, 91, 92, 92, 92, 92, 2, 83, 91, 22, 92, 106, 84, 3795628595, 85, 16921, 86, 46095, 76, 75, 92, 90, 92, 8, 91, 17, 35200, 91, 93, 92, 92, 92, 43, 94, 14, 15, 1, 93, 67, 3323211306, 20, 18, 90, 91, 91, 92, 92, 1, 94, 90, 6, 90, 1, 83, 27640, 67, 3120638745, 75, 22, 92, 91, 92, 91, 90, 0, 83, 91, 22, 91, 106, 84, 3398904215, 85, 51893, 86, 52148, 76, 0, 93, 13, 3893446527, 61, 7, 92, 92, 92, 91, 93, 92, 92, 1, 83, 24474, 67, 3650533271, 107, 22, 91, 90, 92, 91, 92, 2, 83, 90, 22, 91, 106, 84, 3668231371, 85, 12161, 86, 48039, 76, 1, 83, 13551, 71, 92, 92, 90, 92, 92, 90, 91, 2, 83, 22, 92, 83, 92, 92, 92, 63, 90, 91, 83, 92, 88, 90, 91, 91, 92, 91, 92, 64, 92, 91, 92, 88, 91, 88, 91, 90, 91, 92, 105, 84, 2032305046, 89, 3931837303, 65, 92, 89, 90, 151, 92, 92, 898067984, 84, 90, 89, 92, 91, 66, 1, 91, 90, 92, 90, 92, 91, 91, 91, 89, 92, 89, 88, 91, 67, 3323211306, 20, 84, 91, 92, 92, 91, 92, 2, 89, 92, 84, 92, 105, 85, 32162, 86, 45100, 76, 1, 83, 4334, 65, 91, 22, 91, 152, 91, 91, 2152171137, 83, 91, 22, 91, 90, 106, 84, 4002151892, 85, 63096, 86, 42943, 76, 1, 83, 5786, 67, 1382291895, 29, 22, 91, 92, 90, 91, 91, 2, 83, 91, 22, 91, 106, 84, 248918724, 85, 49544, 86, 34677, 76, 73, 9, 92, 90, 10, 91, 93, 92, 91, 90, 92, 61, 1, 92, 90, 91, 91, 93, 92, 91, 109, 11, 153, 2304474209, 83, 15620, 65, 90, 22, 92, 154, 91, 92, 2214856895, 83, 92, 22, 91, 90, 106, 84, 2018164101, 85, 14023, 86, 7681, 76, 61, 6, 90, 92, 90, 90, 18, 92, 92, 1, 83, 13330, 65, 92, 22, 92, 155, 92, 90, 2820653710, 83, 92, 22, 90, 91, 106, 84, 2081602140, 85, 14832, 86, 6713, 76, 112, 93, 79, 83563161, 15, 14, 93, 16, 81, 1263086969, 1, 83, 2603, 67, 3120638745, 75, 22, 91, 91, 91, 91, 91, 1, 83, 91, 22, 91, 106, 84, 243506538, 85, 22242, 86, 34760, 76, 0, 93, 156, 2557096677, 67, 2382441246, 31, 9, 90, 90, 92, 90, 90, 1, 93, 90, 1, 91, 109, 10, 13, 3893446527, 83, 25740, 67, 379555796, 76, 22, 92, 92, 91, 90, 90, 0, 83, 92, 22, 92, 106, 84, 500552537, 85, 8435, 86, 30838, 76, 71, 90, 90, 90, 92, 92, 90, 92, 1, 93, 7, 90, 3, 92, 92, 92, 63, 92, 90, 93, 92, 88, 92, 91, 92, 92, 92, 92, 64, 92, 90, 91, 88, 92, 88, 90, 91, 92, 92, 105, 84, 3284994000, 89, 855971872, 65, 91, 89, 90, 157, 92, 91, 4204407605, 84, 90, 89, 92, 92, 66, 0, 92, 92, 90, 90, 91, 92, 92, 91, 89, 91, 89, 88, 92, 67, 3990410180, 38, 84, 92, 92, 92, 92, 91, 2, 89, 91, 84, 91, 105, 85, 27699, 86, 28431, 76, 1, 83, 38115, 65, 90, 22, 90, 158, 90, 90, 827513844, 83, 91, 22, 90, 92, 106, 84, 264911564, 85, 47488, 86, 34433, 76, 1, 83, 43507, 65, 90, 22, 92, 159, 91, 91, 716048507, 83, 90, 22, 90, 92, 106, 84, 2318706456, 85, 19764, 86, 3095, 76, 45, 6, 116, 5, 93, 86, 449554457, 61, 6, 92, 92, 92, 92, 93, 92, 92, 1, 83, 26960, 65, 92, 22, 90, 160, 90, 91, 3776442572, 83, 92, 22, 91, 90, 106, 84, 2844843849, 85, 5379, 86, 60603, 76, 42, 84, 87, 3, 84, 85, 86, 104, 117, 9, 13, 3893446527, 6, 161, 1843569954, 8, 162, 3317374457, 7, 163, 4113995830, 4, 0, 10, 164, 721934357, 72, 6627, 11, 10, 7, 2, 6, 8, 7, 73, 1, 7, 6, 11, 7, 10, 6, 7, 7, 6, 0, 11, 13, 3893446527, 74, 7, 6, 10, 7, 7, 7, 6, 6, 7, 7, 12, 11, 61, 5, 6, 6, 6, 6, 12, 7, 6, 0, 10, 165, 493955688, 72, 6677, 11, 10, 6, 0, 7, 8, 7, 0, 10, 153, 2304474209, 74, 7, 7, 0, 6, 6, 6, 6, 7, 7, 6, 12, 10, 73, 11, 6, 6, 12, 6, 10, 6, 6, 7, 6, 0, 11, 13, 3893446527, 74, 7, 6, 10, 6, 6, 6, 7, 7, 6, 7, 12, 11, 61, 0, 7, 6, 7, 7, 12, 6, 7, 0, 10, 166, 3294134177, 74, 6, 6, 0, 7, 6, 7, 7, 7, 7, 6, 11, 10, 75, 6, 7, 6, 11, 6, 0, 2933, 6, 10, 6, 7, 7, 0, 11, 13, 3893446527, 74, 7, 6, 10, 6, 7, 7, 7, 7, 6, 7, 12, 11, 61, 0, 7, 6, 6, 7, 12, 7, 7, 119, 10, 63, 11777286, 11, 167, 2176663301, 12, 10, 11, 13, 168, 1561147066, 14, 10, 12, 2, 0, 13, 0, 10, 13, 3893446527, 74, 6, 7, 14, 6, 6, 6, 6, 6, 7, 6, 11, 10, 61, 0, 6, 7, 7, 7, 11, 6, 6, 0, 10, 169, 921220449, 67, 110229586, 40, 11, 7, 6, 7, 6, 7, 1, 10, 7, 0, 6, 0, 10, 13, 3893446527, 74, 7, 6, 11, 6, 6, 6, 6, 6, 7, 6, 12, 10, 61, 0, 7, 6, 6, 6, 12, 6, 6, 0, 10, 170, 165468334, 65, 6, 11, 6, 171, 6, 7, 3885109820, 10, 6, 0, 6, 6, 0, 10, 13, 3893446527, 74, 7, 6, 11, 6, 6, 7, 7, 6, 6, 6, 12, 10, 61, 0, 6, 7, 6, 6, 12, 6, 6, 0, 10, 172, 2815235019, 65, 6, 11, 7, 173, 7, 6, 37608653, 10, 6, 0, 6, 6, 0, 10, 13, 3893446527, 74, 7, 6, 11, 6, 6, 7, 7, 6, 6, 7, 12, 10, 61, 0, 7, 6, 6, 7, 12, 6, 7, 67, 2156355304, 42, 10, 6, 7, 7, 7, 6, 2, 5, 6, 0, 6, 0, 11, 13, 3893446527, 74, 7, 7, 10, 6, 7, 6, 7, 7, 7, 7, 12, 11, 61, 0, 7, 6, 6, 7, 12, 6, 6, 45, 0, 131, 10, 11, 0, 10, 110, 10, 13, 3893446527, 9, 174, 3043977474, 6, 175, 1863623738, 8, 176, 416378402, 125, 7, 177, 3932498152, 4, 11, 164, 721934357, 72, 7039, 12, 11, 8, 2, 7, 9, 8, 73, 1, 7, 6, 12, 7, 11, 6, 7, 6, 6, 0, 12, 13, 3893446527, 74, 6, 7, 11, 8, 7, 6, 7, 6, 6, 6, 13, 12, 61, 5, 8, 7, 7, 6, 13, 6, 7, 67, 3650533271, 107, 11, 7, 6, 7, 7, 6, 0, 5, 6, 0, 6, 0, 12, 13, 3893446527, 74, 6, 7, 11, 7, 6, 7, 7, 7, 8, 7, 13, 12, 61, 0, 6, 7, 7, 8, 13, 8, 8, 0, 11, 178, 3872755641, 74, 6, 6, 0, 8, 6, 7, 6, 7, 7, 6, 12, 11, 75, 6, 6, 8, 12, 7, 0, 23201, 7, 11, 8, 8, 7, 0, 12, 13, 3893446527, 74, 8, 7, 11, 6, 6, 7, 7, 6, 8, 6, 13, 12, 61, 0, 8, 8, 7, 6, 13, 7, 6, 0, 11, 179, 2811570793, 75, 8, 8, 6, 11, 6, 0, 27593, 7, 12, 8, 6, 7, 0, 11, 13, 3893446527, 74, 6, 7, 12, 8, 7, 6, 6, 7, 7, 7, 13, 11, 61, 0, 7, 8, 7, 8, 13, 6, 8, 0, 11, 180, 4286520681, 75, 8, 6, 8, 11, 7, 0, 68, 7, 12, 6, 8, 7, 0, 11, 13, 3893446527, 74, 6, 7, 12, 7, 6, 6, 8, 8, 8, 7, 13, 11, 61, 0, 6, 7, 8, 8, 13, 8, 8, 0, 11, 181, 3237598956, 67, 11777286, 63, 12, 7, 8, 7, 7, 7, 1, 11, 8, 0, 7, 0, 11, 13, 3893446527, 74, 8, 6, 12, 8, 6, 7, 8, 8, 7, 7, 13, 11, 61, 0, 8, 7, 8, 6, 13, 7, 8, 119, 11, 63, 11777286, 12, 167, 2176663301, 13, 11, 12, 14, 182, 3165510105, 15, 11, 13, 2, 0, 14, 0, 11, 13, 3893446527, 74, 6, 8, 15, 8, 6, 8, 6, 8, 6, 8, 12, 11, 61, 0, 8, 6, 6, 8, 12, 8, 7, 45, 0, 131, 11, 11, 0, 11]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 19; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 76: + { + // JUMP_6592 (specialized) + regs[fp + SLOTS.PC] = 6592; + break; + } + case 77: + { + // JUMP_2873 (specialized) + regs[fp + SLOTS.PC] = 2873; + break; + } + case 78: + { + // LOAD_CONST_13_0_1283212058 (specialized) + var dst = 13; + regs[base + dst] = this._constant(0, 1283212058); + break; + } + case 79: + { + // LOAD_CONST_14_1_535688009 (specialized) + var dst = 14; + regs[base + dst] = this._constant(1, 535688009); + break; + } + case 80: + { + // LOAD_CONST_15_2_3730383356 (specialized) + var dst = 15; + regs[base + dst] = this._constant(2, 3730383356); + break; + } + case 81: + { + // LOAD_CONST_8_3_1293333495 (specialized) + var dst = 8; + regs[base + dst] = this._constant(3, 1293333495); + break; + } + case 82: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 83: + { + // LOAD_GLOBAL_16_10_3431107281 (specialized) + var dst = 16; + var globalName = this._constant(10, 3431107281); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 84: + { + // LOAD_INT_10_195148527 (specialized) + var dst = 10; + regs[base + dst] = 195148527; + break; + } + case 85: + { + // LOAD_INT_11_143 (specialized) + var dst = 11; + regs[base + dst] = 143; + break; + } + case 86: + { + // LOAD_INT_12_11 (specialized) + var dst = 12; + regs[base + dst] = 11; + break; + } + case 87: + { + // SET_PROP_16_5_17 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 16]; + var key = regs[base + 5]; + var val = regs[base + 17]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 88: + { + // RETURN_16 (specialized) + var retVal = regs[base + 16]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 89: + { + // LOAD_THIS_15 (specialized) + var dst = 15; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 90: + { + // LOAD_INT_67_1879415178 (specialized) + var dst = 67; + regs[base + dst] = 1879415178; + break; + } + case 91: + { + // GET_PROP_78_3_77 (specialized) + // dst = regs[obj][regs[key]] + var dst = 78; + var obj = regs[base + 3]; + var key = regs[base + 77]; + regs[base + dst] = obj[key]; + break; + } + case 92: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 93: + { + // LOAD_GLOBAL_77_78_3044920602 (specialized) + var dst = 77; + var globalName = this._constant(78, 3044920602); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 94: + { + // RETURN_4 (specialized) + var retVal = regs[base + 4]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 95: + { + // LOAD_GLOBAL_7_72_1686888730 (specialized) + var dst = 7; + var globalName = this._constant(72, 1686888730); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 96: + { + // GET_PROP_8_7_77 (specialized) + // dst = regs[obj][regs[key]] + var dst = 8; + var obj = regs[base + 7]; + var key = regs[base + 77]; + regs[base + dst] = obj[key]; + break; + } + case 97: + { + // GET_PROP_11_3_77 (specialized) + // dst = regs[obj][regs[key]] + var dst = 11; + var obj = regs[base + 3]; + var key = regs[base + 77]; + regs[base + dst] = obj[key]; + break; + } + case 98: + { + // RETURN_77 (specialized) + var retVal = regs[base + 77]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 99: + { + // GET_PROP_78_3_77 (specialized) + // dst = regs[obj][regs[key]] + var dst = 78; + var obj = regs[base + 3]; + var key = regs[base + 77]; + regs[base + dst] = obj[key]; + break; + } + case 100: + { + // JUMP_REG_67 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 67]; + break; + } + case 101: + { + // LOAD_THIS_20 (specialized) + var dst = 20; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 102: + { + // RETURN_93 (specialized) + var retVal = regs[base + 93]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 103: + { + // LOAD_GLOBAL_14_72_1686888730 (specialized) + var dst = 14; + var globalName = this._constant(72, 1686888730); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 104: + { + // JUMP_REG_84 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 84]; + break; + } + case 105: + { + // LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 106: + { + // LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 107: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 108: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 109: + { + // LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 110: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 111: + { + // LOAD_CONST,LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 112: + { + // LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 113: + { + // LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 114: + { + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 115: + { + // GET_PROP,LOAD_CONST (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 116: + { + // LOAD_THIS,LOAD_CONST (macro) + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 117: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 118: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 119: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 120: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 121: + { + // LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 122: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 123: + { + // LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 124: + { + // LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 125: + { + // LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 126: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 127: + { + // GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 128: + { + // LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 129: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 130: + { + // LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 131: + { + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 61: + { + // ANTI_MOVE_3_7_0_1_5_4_6_2 (order: [3,7,0,1,5,4,6,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[1]]; + // SHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >> regs[base + _operands[2]]; + // MOVE + var dst = _operands[3]; + regs[base + dst] = regs[base + _operands[4]]; + // LT + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a < regs[base + _operands[7]]; + break; + } + case 62: + { + // ANTI_NEQ_5_4_6_9_0_2_3_7_1_8_10 (order: [5,4,6,9,0,2,3,7,1,8,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[9], _unsortedOperands[3], _unsortedOperands[10]]; + // UNARY_POS + var dst = _operands[0]; + regs[base + dst] = +regs[base + _operands[1]]; + // SUB + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a - regs[base + _operands[4]]; + // NEQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a !== regs[base + _operands[7]]; + // LOAD_CONST + var dst = _operands[8]; + regs[base + dst] = this._constant(_operands[9], _operands[10]); + break; + } + case 63: + { + // ANTI_UNARY_NOT_2_1_4_5_3_7_8_0_6_10_9 (order: [2,1,4,5,3,7,8,0,6,10,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[9]]; + // MUL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a * regs[base + _operands[2]]; + // UNARY_NOT + var dst = _operands[3]; + regs[base + dst] = !regs[base + _operands[4]]; + // DIV + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a / regs[base + _operands[7]]; + // NEQ + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a !== regs[base + _operands[10]]; + break; + } + case 64: + { + // ANTI_UNARY_POS_8_5_0_6_2_7_1_4_9_3 (order: [8,5,0,6,2,7,1,4,9,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[8]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // SHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >> regs[base + _operands[5]]; + // UNARY_POS + var dst = _operands[6]; + regs[base + dst] = +regs[base + _operands[7]]; + // TYPEOF + var dst = _operands[8]; + regs[base + dst] = typeof regs[base + _operands[9]]; + break; + } + case 65: + { + // ANTI_SUB_2_3_11_7_10_0_8_5_9_4_1_6 (order: [2,3,11,7,10,0,8,5,9,4,1,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[2]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // SUB + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a - regs[base + _operands[5]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[6]; + var name = this._constant(_operands[7], _operands[8]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // BAND + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a & regs[base + _operands[11]]; + break; + } + case 66: + { + // ANTI_MUL_1_8_12_7_10_11_13_9_2_3_6_4_5_0 (order: [1,8,12,7,10,11,13,9,2,3,6,4,5,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[13], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[6]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[0]; + var count = _operands[1]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[2]]; + regs[base + dst] = elems; + // MUL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a * regs[base + _operands[5]]; + // SHR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >> regs[base + _operands[8]]; + // UNARY_NEG + var dst = _operands[9]; + regs[base + dst] = -regs[base + _operands[10]]; + // GTE + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a >= regs[base + _operands[13]]; + break; + } + case 67: + { + // ANTI_ADD_5_4_0_8_3_12_6_11_7_2_10_1_9 (order: [5,4,0,8,3,12,6,11,7,2,10,1,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[11], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[12], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[5]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // LOAD_GLOBAL + var dst = _operands[3]; + var globalName = this._constant(_operands[4], _operands[5]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[6]; + var pairCount = _operands[7]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[8]]; + var val = regs[base + _operands[9]]; + o[key] = val; + } + regs[base + dst] = o; + // LT + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a < regs[base + _operands[12]]; + break; + } + case 68: + { + // ANTI_EQ_5_10_7_6_3_9_2_1_12_8_4_0_11 (order: [5,10,7,6,3,9,2,1,12,8,4,0,11]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[11], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[12], _unsortedOperands[8]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // EQ + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a === regs[base + _operands[4]]; + // SHL + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a << regs[base + _operands[7]]; + // LTE + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a <= regs[base + _operands[10]]; + // UNARY_NOT + var dst = _operands[11]; + regs[base + dst] = !regs[base + _operands[12]]; + break; + } + case 69: + { + // ANTI_UNARY_NEG_4_5_3_6_1_0_2 (order: [4,5,3,6,1,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[3]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // UNARY_NEG + var dst = _operands[2]; + regs[base + dst] = -regs[base + _operands[3]]; + // ADD + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a + regs[base + _operands[6]]; + break; + } + case 70: + { + // ANTI_BAND_12_6_9_8_2_11_10_3_7_1_5_4_0_13 (order: [12,6,9,8,2,11,10,3,7,1,5,4,0,13]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[12], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[13]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // LOOSE_EQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a == regs[base + _operands[5]]; + // MOD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a % regs[base + _operands[8]]; + // BXOR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a ^ regs[base + _operands[11]]; + // MOVE + var dst = _operands[12]; + regs[base + dst] = regs[base + _operands[13]]; + break; + } + case 71: + { + // ANTI_LT_0_1_12_7_3_4_6_13_9_10_2_11_14_8_5 (order: [0,1,12,7,3,4,6,13,9,10,2,11,14,8,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[14], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[13], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[11], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[12]]; + // DIV + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a / regs[base + _operands[2]]; + // EQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a === regs[base + _operands[5]]; + // GTE + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >= regs[base + _operands[8]]; + // LT + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a < regs[base + _operands[11]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[12]; + var count = _operands[13]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[14]]; + regs[base + dst] = elems; + break; + } + case 72: + { + // ANTI_SHL_1_2_4_5_3_7_0_6 (order: [1,2,4,5,3,7,0,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[5]]; + // JUMP_IF_FALSE + var src = _operands[0]; + var target = _operands[1]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // SHL + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a << regs[base + _operands[4]]; + // EQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a === regs[base + _operands[7]]; + break; + } + case 73: + { + // ANTI_BOR_1_6_3_2_7_0_8_5_4_9 (order: [1,6,3,2,7,0,8,5,4,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[9]]; + // BOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a | regs[base + _operands[2]]; + // UNARY_NEG + var dst = _operands[3]; + regs[base + dst] = -regs[base + _operands[4]]; + // LTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a <= regs[base + _operands[7]]; + // UNARY_POS + var dst = _operands[8]; + regs[base + dst] = +regs[base + _operands[9]]; + break; + } + case 74: + { + // ANTI_USHR_6_3_1_7_4_5_9_10_11_8_0_2 (order: [6,3,1,7,4,5,9,10,11,8,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[2], _unsortedOperands[11], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[8]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a !== regs[base + _operands[5]]; + // LT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a < regs[base + _operands[8]]; + // GTE + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a >= regs[base + _operands[11]]; + break; + } + case 75: + { + // ANTI_BXOR_2_7_6_11_1_10_8_0_9_3_4_5 (order: [2,7,6,11,1,10,8,0,9,3,4,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[11], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[3]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // LT + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a < regs[base + _operands[4]]; + // UNARY_POS + var dst = _operands[5]; + regs[base + dst] = +regs[base + _operands[6]]; + // LOAD_INT + var dst = _operands[7]; + regs[base + dst] = _operands[8]; + // BXOR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a ^ regs[base + _operands[11]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/qualification.json b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/qualification.json new file mode 100644 index 00000000..8d40b85a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/qualification.json @@ -0,0 +1,105 @@ +{ + "id": "repeatedArrays--stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodes--99", + "case": "repeatedArrays", + "profile": "stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodes", + "seed": 99, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": true, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ], + [ + 5, + 6 + ] + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ], + [ + 5, + 6 + ] + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ], + [ + 5, + 6 + ] + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true, + "concealConstants": true, + "antiInstrumentation": true, + "specializedOpcodes": true, + "macroOpcodes": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/source.js b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/source.js new file mode 100644 index 00000000..d9e21348 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-seven-option-repeated-arrays/source.js @@ -0,0 +1 @@ +const a = [1, 2]; const b = [3, 4]; const c = [5, 6]; window.TEST_OUTPUT = [a, b, c]; diff --git a/test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/encoded.js new file mode 100644 index 00000000..487b672b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/encoded.js @@ -0,0 +1,5266 @@ +var CONSTANTS = [/* 0 */"I76FKj3m930K0hVWhsMKzqdSHfa3bSGatzYcit8f/Irr9uSd1y23piJSz88gQpfG4U33Pe2mkmr3//oLzob2OuuNLxW7YvzvqlPCNvrCkF3/9dNqE9+rqxE3rZowDbuFIBWprtIjEKeY0m59u8UBRYi+AyvhZ2r7e21fFVCFVw1vk893UOOgXke1QzV1XVfLeHd423svNKVZFV6NtvwWh2wTdL9mJR7lCH18zWlHVTsgz6hUNeW2bTadvTRzg6YfcnME9YadCC2atDhbmM8Ye6PUNa3YPdmkjBTN//0DsoQbPcmtFOX/ZNLv+1vRxNfM313F9fdk4RyBsx800JziDd315xTqrMg78VevzEB8kcULVJq8Liy8Z2H824yJBTWEtAwJnOJnDezCXDW0mcUvXE3MXHQM3Ecsb7SnFSGMs/wAlHsseLw+JA3UfHxQzGpEMjR6rLFkAuR+bHicfAROhAscVHRC5JRdHSyXtDVEjs8mdIHUKKxdPLSUdBT1/wYE/5QcPKitIeS3dN/P11vvxPTM1V015J5kDRyYs+AE0ZzDDdj12GTOrMo7+lfwzO18mtU1Vee80SPsl+D84IzsBZC1pCxfm4hnGeuqXBHFtMUDXdTsrGci45AsILWfBQSNmPxil6wrZL95VVPVGH12zR5X3DM2r4ZkW/VrDW6dMQdxi3cfc2tX5WtdMM2QxldDes98cxnlXK0TPWSVGQuc/x0L0JY0PYGtKNWvchXPdUOXti0trl0D5dVqQx+CiwcHh5rHbYTl4GLFrt0zLkemwhh9m9XyKqW+vCvvl+j63I7aFey18i4A4pt3oPLUXtXF+bXyXtXqlWcRmpEOQ7rXBT2OsfJ0Zv8jiL5yUpPFIH6FyhtWsjsMrrIaBfYeDkaCUjZCsm0ee2Id9oRdRsqaxnZae+5fenbmZKo5Ml+GZwJx/hQSuqZ9IkyqZNZden7OS1oXtnQqv2EN9o1yZR6VgwY2rpJbbj7mmloXzoA7eUaGyhl+sOluIvy+9hPbh/gC2o7dERG6wy4U2vd3+cqlXs7J/7HEXq7i6Xeyk8IOSoG4Gadu7Pr+Z/QrnL7LSpXBFHmf0imnviMar7ESZfn6AdeaDTeDqyXfkGo38ctZHsHlNgdTGO9hY0IZEpF3OYiGPgum/xprJaFrOURRZdkWQzfPcFMjp0whgZkx8X+LKx97i3w3bZlHYQz591ATz7PDHlej0H157uFwKZSwigsTh6n4HZG8GQOhsygG0+tHLMOEWOQx4KnBINjorndFm84AQ7mhEdZox+DuV9HTw7jFQPDJ7HGu6DWg1TuvkMQYhYHsCeCQ0CiEoxzYznAmyJFBYMjuMH9Y0e+ieCIQi4kQAPiIPxDG/xtgs6hfMHZIZcAQeBbwVFiHoFoom5FMCEuAIBhCsEUoMqBJaFnwc1hLwLi7BFBz6G9gbuhOEE64awBXiJvwKmiWIRq4oDAv2IRHG8haUPcpMKCGGHLwkHcqgJoIb7CTGcBg5/jmV+zLl8A3SKLRGHiU4N6nrTPZiNoA6/nrcciY8CCjqy/fikgIwfNZo7DCOMKjye/ucBEZqoEbOYqQFGvn/wprpNAVOeVBhtg/c57vKSOtuDwxjZlkAaGIGR9pq0onEdlQcR35p0A8z7yzRVd44Tp5P5F3CWe/agt+l3r7P2GH2SKxcU9yw3lHdvNlWXohDam6HiyLjgcTm7sOKLmuIQdpKvOlVw7Dnz4GUZHpDWGn6gmXwDvsjtcaz/HAaeqiEiewsyTfs0LDyd5R+qrAJvub5e/meuQGoYkpMtqW32Pl/5lSzdbJQZlJxNYNeqPvLRqotj0KmoE+BqJy6h8toocmF9KbSR/GjlodzuC7HCZl6k5+c5ZMAnK+WBIrlpjiFW5W1lm6Qk5A2nV2MuphXnUqX7LNHgqCvAcukkIuXqJU2hduTvp1RmzKlz46qp7lan6FEiCW+AMHjhcS7vUXbrjaNjaISzzeukpo9uld4KLyJuSTrw+ugi0Gyo0DOsSmrWu5/+tK+cbJvf9N1NbKYobvNBOVtscNqp3b9tNqnh+Xq4s2Cd2kTRrFmlIbL6ajcyfvkqx9B4W3Cm6//isnZ4D6FG2W1WtFAU9T00p3SON0nao1G7WVHliLU3dbmzotXaVy1VxNTMNpR0XTnzwzpVrlR31CC1yHfDtNj3AVmOUpbc3cPjeLQ23flXR79fJdkKweN46bMg+YhAcVvq0CPemky1PR/85E8MTNvdAtmuQEe4vv4RTQhMkNyZ3n9NVkuN/ws6M02tyLTUf08FTmL8W74RTzjPd95oW5FA28siMgZKj8F0yaxQxE0Ezz2zdki+wMnIY1MISrHCKMtXddnEUsaLRU1E9MV/zXVzKsgTw1pFsUUHxgDF+EajxLrH1kQOQ9bJ/cNER2THrcW3Rs5GYcaKw+NLCbPQycdDQUvK0nPILUIFRq/PpEJdSavR4suOQ5dIWPFgTghM0N552R5PRkrN88tIdESdzeTdf07mTjL+G78RTYjLF91ZW6FDe/kyM5V8j8+03lxcY0lKzr2zJnvew7nboFPIWBHGjzgWcy4y4tr7U/xb9MIfvGV0CrhDwIlRAVG311AxWXVzNKrxhlufVGTZrcZUdNS0PfTGVB5XodTo1qBz6DBw+iciblaa1QPR3XD0tX/0ZLC9W9vQ8tuJZlc4OPMwOdtiMtGJ2+5jtr4d9/S8ZGM92nbWblmmKcL5Gz0Abqgql9GZW0Kj2/xyvwVvT66U231dQy267L4/F3n/IbnqoF+IrxHs/ryGfj2tdO4kUPwsROC/KQZyOisz4tkvgFDX4ACo6XNTq9rj5aifUDTuLCE7Z8MlPeUWIc9nEeKoomBtmbEe5LelDmWq5OMg7GVkJW/npSSCamoTkqipZ/emGOdRpBtkgqfWKr9iZyvN8HQpG2PtKVaS7moZqvD3u6oAYbiqpxT+abIg6+vNNvRsTC8Eny0ZE6", /* 1 */"UOlJcUPbQk90O5jQ", /* 2 */-1054784800, /* 3 */-101517943, /* 4 */undefined, /* 5 */588930384, /* 6 */336357977, /* 7 */-1181100154, /* 8 */467895835, /* 9 */-2136374803, /* 10 */1816815260, /* 11 */-1014234189, /* 12 */-245186102, /* 13 */1887905778, /* 14 */1320319987, /* 15 */422069148, /* 16 */-1653687461, /* 17 */-1895718270, /* 18 */-2128084032, /* 19 */1827334362, /* 20 */1121896884, /* 21 */1576951653, /* 22 */492433720, /* 23 */1441133258, /* 24 */-1930555285, /* 25 */-544228554, /* 26 */-1736242534, /* 27 */1432326694, /* 28 */-1795972655, /* 29 */1858026169, /* 30 */-2143327532, /* 31 */428770395, /* 32 */"4jTJn95r/uTgXA==", /* 33 */true, /* 34 */"05CaKSahuSg/Uw==", /* 35 */-924362364, /* 36 */1662973410, /* 37 */2020529333, /* 38 */-393404633, /* 39 */-1146367173, /* 40 */98857444, /* 41 */1475023033, /* 42 */-1480586868, /* 43 */-1779721737, /* 44 */643992084, /* 45 */1939409469, /* 46 */-894541996, /* 47 */-1490572292, /* 48 */1653073378, /* 49 */646087904, /* 50 */454075887, /* 51 */430196373, /* 52 */-1768602437, /* 53 */-219002303, /* 54 */126073175, /* 55 */"wIvWHsiK5jHdmfxiEPacU8vO8MI=", /* 56 */"ZG1u45BnFhNhvXYF", /* 57 */"", /* 58 */1778635174, /* 59 */"5ATEhf8N//z1ZOir", /* 60 */"4K325eB69+7WmxgHk4oFHav18mLC3sUz", /* 61 */"dGeBKx+/kUQ=", /* 62 */-136239534, /* 63 */-435797673, /* 64 */-2027089461, /* 65 */1363420261, /* 66 */-266154174, /* 67 */-148104241, /* 68 */1448728097, /* 69 */1633139685, /* 70 */695390351, /* 71 */-1562299783, /* 72 */350480009, /* 73 */-276276189, /* 74 */-1348095774, /* 75 */444937552, /* 76 */-305281993, /* 77 */-104997209, /* 78 */-786948595, /* 79 */-1884980552, /* 80 */1103524795, /* 81 */2126327969, /* 82 */-982365515, /* 83 */1822528018, /* 84 */"hhfeej4OrZU=", /* 85 */"clZ5OoSvHho=", /* 86 */1960755905, /* 87 */-441740210, /* 88 */1642615866, /* 89 */-1106010369, /* 90 */-383678591, /* 91 */7961744, /* 92 */-2030129123, /* 93 */-538297424, /* 94 */561929402, /* 95 */2040175696, /* 96 */1175971055, /* 97 */1168667289, /* 98 */-1307057726, /* 99 */-1128124124, /* 100 */154112475]; +var BYTECODE = [47, 6, 1355, 1, 83, 0, 0, 92, 42, 5, 6, 1, 7, 47, 8, 2387, 4, 83, 0, 0, 93, 47, 12, 82, 1, 105, 1, 0, 1, 5, 94, 95, 96, 97, 98, 42, 3, 8, 4, 5, 9, 10, 11, 99, 100, 42, 13, 2, 1, 14, 101, 42, 15, 2, 1, 14, 102, 148, 9, 3776803505, 10, 202, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 103, 48, 14, 3, 13, 15, 16, 104, 105, 106, 47, 100, 3401, 2, 10, 0, 0, 107, 0, 20, 5, 588969227, 108, 0, 22, 6, 336357977, 109, 0, 24, 7, 3113871440, 110, 111, 64, 144, 98, 1125618069, 103, 1075678517, 65, 81, 66, 1, 99, 2994105604, 61, 0, 26, 8, 467911833, 112, 113, 114, 64, 144, 98, 3260219151, 103, 3019165903, 65, 81, 66, 1, 99, 421867770, 61, 144, 98, 2941191821, 99, 2601872444, 61, 152, 28, 9, 2158572957, 29, 23, 28, 23, 29, 115, 34, 102, 30, 64, 144, 98, 2301917953, 103, 2933542081, 65, 84, 66, 1, 99, 2535368944, 61, 144, 98, 95516977, 99, 3711585728, 61, 153, 31, 10, 1816811672, 32, 23, 31, 23, 32, 116, 34, 102, 33, 64, 144, 98, 3964721084, 103, 1686817148, 65, 81, 66, 1, 99, 3445440619, 61, 144, 98, 2947157729, 99, 4164747152, 61, 153, 34, 11, 3280758660, 35, 23, 34, 23, 35, 117, 34, 102, 36, 64, 144, 98, 2724719752, 103, 3322803784, 65, 84, 66, 1, 99, 62991223, 61, 144, 98, 2576234005, 99, 617254213, 61, 155, 37, 12, 4049768624, 38, 23, 37, 23, 38, 39, 21, 23, 102, 39, 64, 73, 144, 98, 1509961564, 103, 195166236, 65, 78, 66, 1, 99, 2607923467, 61, 144, 98, 1090185483, 99, 2007739478, 61, 156, 40, 13, 1887900396, 41, 23, 40, 23, 41, 42, 21, 23, 102, 42, 64, 73, 144, 98, 4101409162, 103, 3107985738, 65, 82, 66, 1, 99, 1025813493, 61, 144, 98, 1940057617, 99, 2348983872, 61, 156, 43, 14, 1320317900, 44, 23, 43, 23, 44, 45, 21, 23, 102, 45, 64, 144, 98, 3073540804, 103, 670929796, 65, 81, 66, 1, 99, 835232531, 61, 144, 98, 966539404, 99, 2921954620, 61, 156, 46, 15, 422061149, 47, 23, 46, 23, 47, 48, 21, 23, 102, 48, 64, 73, 144, 98, 2670626296, 103, 266963640, 65, 78, 66, 1, 99, 2041147335, 61, 144, 98, 3211443284, 99, 2432216419, 61, 156, 49, 16, 2641297045, 50, 23, 49, 23, 50, 51, 21, 23, 102, 51, 64, 73, 144, 98, 1693701987, 103, 330998819, 65, 82, 66, 1, 99, 1906640366, 61, 144, 98, 915297574, 99, 467369777, 61, 155, 52, 17, 2399216079, 53, 23, 52, 23, 53, 54, 21, 23, 102, 54, 64, 73, 144, 98, 2347980366, 103, 1892458766, 65, 82, 66, 1, 99, 1156308185, 61, 144, 98, 1460544596, 99, 3146368611, 61, 155, 55, 18, 2166895528, 56, 23, 55, 23, 56, 57, 21, 23, 102, 57, 64, 144, 98, 3145027573, 103, 1615781557, 65, 81, 66, 1, 99, 3965046564, 61, 144, 98, 1121112508, 99, 1176653708, 61, 155, 58, 19, 1827330195, 59, 23, 58, 23, 59, 60, 21, 23, 102, 60, 64, 73, 144, 98, 3167001756, 103, 3063967324, 65, 78, 66, 1, 99, 467366347, 61, 144, 98, 3018627839, 99, 2874568938, 61, 156, 61, 20, 1121895830, 62, 23, 61, 23, 62, 63, 21, 23, 102, 63, 64, 73, 144, 98, 4292815343, 103, 4189797039, 65, 78, 66, 1, 99, 3037563034, 61, 144, 98, 622416778, 99, 1290351733, 61, 156, 64, 21, 1576962462, 65, 23, 64, 23, 65, 66, 21, 23, 102, 66, 64, 144, 98, 3267674425, 103, 3701527289, 65, 84, 66, 1, 99, 914345160, 61, 144, 98, 1994200276, 99, 2417232772, 61, 155, 67, 22, 492412722, 68, 23, 67, 23, 68, 69, 21, 23, 102, 69, 64, 73, 144, 98, 3856173328, 103, 2838632656, 65, 78, 66, 1, 99, 3428021919, 61, 144, 98, 1004249669, 99, 46566196, 61, 156, 70, 23, 1441129907, 71, 23, 70, 23, 71, 72, 21, 23, 102, 72, 64, 73, 144, 98, 3551757037, 103, 1115329453, 65, 78, 66, 1, 99, 768689404, 61, 144, 98, 2226256163, 99, 810954286, 61, 155, 73, 24, 2364438786, 74, 23, 73, 23, 74, 75, 21, 23, 102, 75, 64, 73, 144, 98, 4247923918, 103, 1845312910, 65, 78, 66, 1, 99, 3751504601, 61, 144, 98, 467073853, 99, 3095942349, 61, 156, 76, 25, 3750735993, 77, 23, 76, 23, 77, 78, 21, 23, 102, 78, 64, 73, 144, 98, 3268413310, 103, 1905988670, 65, 82, 66, 1, 99, 726554633, 61, 144, 98, 2404181099, 99, 3357733398, 61, 156, 79, 26, 2558760500, 80, 23, 79, 23, 80, 81, 21, 23, 102, 81, 64, 144, 98, 1155638130, 103, 4087918642, 65, 81, 66, 1, 99, 1629796317, 61, 144, 98, 3071598736, 99, 3145346463, 61, 155, 82, 27, 1432312312, 83, 23, 82, 23, 83, 84, 21, 23, 102, 84, 64, 144, 98, 3294557705, 103, 2309505481, 65, 84, 66, 1, 99, 260108952, 61, 144, 98, 4077899578, 99, 2705939205, 61, 156, 85, 28, 2498974974, 86, 23, 85, 23, 86, 87, 21, 23, 102, 87, 64, 73, 144, 98, 2084181038, 103, 2178947566, 65, 78, 66, 1, 99, 1399231545, 61, 144, 98, 1332726019, 99, 1562895182, 61, 155, 88, 29, 1858054309, 89, 23, 88, 23, 89, 90, 21, 23, 102, 90, 64, 73, 144, 98, 3600637191, 103, 785864391, 65, 78, 66, 1, 99, 702295826, 61, 144, 98, 3987407296, 99, 3737700560, 61, 155, 91, 30, 2151621000, 92, 23, 91, 23, 92, 93, 21, 23, 102, 93, 64, 73, 144, 98, 4054357508, 103, 358521796, 65, 82, 66, 1, 99, 2452213203, 61, 144, 98, 1115782156, 99, 931008123, 61, 155, 94, 31, 428773596, 95, 23, 94, 23, 95, 96, 21, 23, 102, 96, 64, 73, 144, 98, 1740675184, 103, 723381040, 65, 82, 66, 1, 99, 942612927, 61, 144, 98, 246923028, 99, 3432328547, 61, 144, 98, 1212538890, 99, 2677227221, 61, 118, 148, 16, 3705931324, 17, 213, 97, 10818, 77, 144, 98, 2957059918, 99, 1469053977, 61, 1, 21, 14427, 119, 148, 16, 3776803505, 17, 202, 97, 40176, 74, 144, 98, 119366429, 99, 3762012140, 61, 55, 144, 4, 1335, 97, 66, 77, 144, 98, 3606857138, 99, 836557117, 61, 55, 1, 97, 50687, 77, 144, 98, 1761677133, 99, 2119181340, 61, 1, 4, 1111, 55, 1, 97, 18147, 77, 144, 98, 1024385167, 99, 3394125402, 61, 1, 97, 41255, 85, 86, 64, 73, 144, 98, 1771528817, 103, 2522772273, 65, 78, 66, 1, 99, 195544512, 61, 1, 97, 11824, 74, 144, 98, 3128162484, 99, 1298224707, 61, 1, 97, 0, 74, 144, 98, 824708424, 99, 3593019927, 61, 1, 97, 46542, 77, 144, 98, 1021872961, 99, 3421022224, 61, 120, 47, 15, 2387, 4, 83, 0, 0, 121, 1, 97, 28135, 77, 144, 98, 1106934702, 99, 2799359801, 61, 1, 97, 15604, 85, 86, 64, 144, 98, 3159912007, 103, 2492529927, 65, 84, 66, 1, 99, 419395954, 61, 1, 97, 14423, 77, 144, 98, 1761549643, 99, 2145268246, 61, 1, 97, 15548, 74, 144, 98, 4227488561, 99, 215676864, 61, 122, 123, 1, 97, 57509, 74, 144, 98, 2725180272, 99, 1164857343, 61, 1, 21, 8484, 88, 42, 10, 15, 4, 14, 16, 17, 18, 124, 179, 5, 104, 97, 14768, 77, 144, 98, 164677322, 99, 3741342869, 61, 1, 97, 37754, 74, 144, 98, 2718954639, 99, 1162693210, 61, 125, 34, 102, 104, 64, 73, 144, 98, 1183137470, 103, 2643854462, 65, 82, 66, 1, 99, 1178820617, 61, 1, 97, 816, 74, 144, 98, 493991706, 99, 3932130277, 61, 1, 97, 24943, 74, 144, 98, 667285790, 99, 3239269865, 61, 1, 21, 38583, 88, 1, 21, 63218, 126, 149, 104, 2, 3240182497, 9, 0, 104, 55, 1, 97, 57515, 74, 144, 98, 1277838327, 99, 2603278210, 61, 89, 42, 12, 15, 4, 14, 16, 17, 18, 183, 13, 33, 0, 97, 48894, 77, 144, 98, 2724449924, 99, 1165059155, 61, 283, 21, 27776, 16, 1359064005, 17, 226, 18, 7, 1, 97, 19566, 74, 144, 98, 3509435110, 99, 908826737, 61, 179, 5, 9, 4, 1184, 55, 1, 97, 46317, 77, 144, 98, 3889369770, 99, 16920629, 61, 89, 42, 8, 15, 4, 14, 16, 17, 18, 44, 104, 7, 1, 8, 127, 1, 97, 36797, 85, 86, 64, 144, 98, 4106286984, 103, 3784033608, 65, 81, 66, 1, 99, 4144340184, 61, 1, 97, 15758, 74, 144, 98, 2104769124, 99, 2321881331, 61, 1, 97, 20976, 74, 144, 98, 2663541005, 99, 1779615196, 61, 128, 129, 183, 6, 6, 336357977, 97, 12551, 77, 144, 98, 599553723, 99, 3298610246, 61, 1, 21, 21687, 0, 104, 4, 0, 130, 42, 98, 100, 2, 98, 99, 0, 101, 34, 1463730514, 131, 132, 47, 78, 3542, 2, 10, 0, 0, 133, 284, 16, 35, 3370598215, 17, 16, 18, 6, 336357977, 19, 18, 0, 20, 36, 1662944668, 134, 34, 79, 21, 67, 144, 76, 2204004284, 80, 487545867, 68, 83, 69, 1, 77, 2232396738, 62, 285, 22, 35, 3370598215, 19, 22, 23, 17, 19, 79, 23, 67, 75, 144, 76, 1729095790, 80, 2732361674, 68, 79, 69, 1, 77, 4085717495, 62, 144, 76, 1491997550, 77, 2329742376, 62, 156, 24, 37, 2020509743, 25, 19, 24, 19, 25, 26, 17, 19, 79, 26, 67, 144, 76, 2207672385, 80, 3210938211, 68, 83, 69, 1, 77, 1142424608, 62, 144, 76, 1915318826, 77, 2745401154, 62, 155, 27, 38, 3901556673, 28, 19, 27, 19, 28, 29, 17, 19, 79, 29, 67, 144, 76, 95184706, 80, 1098449830, 68, 83, 69, 1, 77, 1550227964, 62, 144, 76, 1883061110, 77, 3177740221, 62, 155, 30, 39, 3148598912, 31, 19, 30, 19, 31, 32, 17, 19, 79, 32, 67, 75, 144, 76, 1644918101, 80, 2681737811, 68, 79, 69, 1, 77, 3358842753, 62, 144, 76, 1124885447, 77, 1509850103, 62, 156, 33, 40, 98829276, 34, 19, 33, 19, 34, 35, 17, 19, 79, 35, 67, 144, 76, 2709888381, 80, 3713153087, 68, 87, 69, 1, 77, 2459396411, 62, 144, 76, 1310792776, 77, 233780059, 62, 155, 36, 41, 1475067500, 37, 19, 36, 19, 37, 38, 17, 19, 79, 38, 67, 75, 144, 76, 2705757428, 80, 3709022900, 68, 16, 80, 80, 79, 69, 1, 77, 1009847234, 62, 144, 76, 1708330327, 77, 413932061, 62, 155, 39, 42, 2814378035, 40, 19, 39, 19, 40, 41, 17, 19, 79, 41, 67, 144, 76, 353844376, 80, 1357109592, 68, 83, 69, 1, 77, 836171415, 62, 144, 76, 1931843420, 77, 2912842271, 62, 156, 42, 43, 2515209188, 43, 19, 42, 19, 43, 44, 17, 19, 79, 44, 67, 75, 144, 76, 348241817, 80, 1351506523, 68, 79, 69, 1, 77, 1129939309, 62, 144, 76, 1650877861, 77, 2198450920, 62, 155, 45, 44, 644018614, 46, 19, 45, 19, 46, 47, 17, 19, 79, 47, 67, 75, 144, 76, 4133566536, 80, 841864727, 68, 79, 69, 1, 77, 1331406900, 62, 144, 76, 744841555, 77, 3632040197, 62, 155, 48, 45, 1939421928, 49, 19, 48, 19, 49, 50, 17, 19, 79, 50, 67, 144, 76, 1324083616, 80, 2327349696, 68, 87, 69, 1, 77, 2718317435, 62, 144, 76, 2945306929, 77, 13533564, 62, 156, 51, 46, 3400421147, 52, 19, 51, 19, 52, 53, 17, 19, 79, 53, 67, 75, 144, 76, 1209249187, 80, 2246069701, 68, 16, 80, 80, 79, 69, 1, 77, 4278527231, 62, 144, 76, 4047645019, 77, 3759771103, 62, 156, 54, 47, 2804415583, 55, 19, 54, 19, 55, 56, 17, 19, 79, 56, 67, 144, 76, 4039446630, 80, 747744833, 68, 83, 69, 1, 77, 1694936302, 62, 144, 76, 3299644574, 77, 1983678474, 62, 156, 57, 48, 1653062605, 58, 19, 57, 19, 58, 59, 17, 19, 79, 59, 67, 75, 144, 76, 2458021447, 80, 3461287013, 68, 16, 80, 80, 79, 69, 1, 77, 215176774, 62, 144, 76, 1413742529, 77, 2462589768, 62, 155, 60, 49, 646113565, 61, 19, 60, 19, 61, 62, 17, 19, 79, 62, 67, 75, 144, 76, 903834745, 80, 1907101243, 68, 79, 69, 1, 77, 4153872237, 62, 144, 76, 3195058340, 77, 3801655639, 62, 155, 63, 50, 454070811, 64, 19, 63, 19, 64, 65, 17, 19, 79, 65, 67, 75, 144, 76, 2920052738, 80, 3956872614, 68, 79, 69, 1, 77, 1247810432, 62, 144, 76, 396781906, 77, 662413387, 62, 155, 66, 51, 430179967, 67, 19, 66, 19, 67, 68, 17, 19, 79, 68, 67, 75, 144, 76, 401662610, 80, 1404927826, 68, 79, 69, 1, 77, 2027426698, 62, 144, 76, 2321103867, 77, 1230103763, 62, 156, 69, 52, 2526380254, 70, 19, 69, 19, 70, 71, 17, 19, 79, 71, 67, 144, 76, 3515469690, 80, 223768511, 68, 83, 69, 1, 77, 1386247426, 62, 144, 76, 1455466393, 77, 2077527882, 62, 155, 72, 53, 4075962517, 73, 19, 72, 19, 73, 74, 17, 19, 79, 74, 67, 75, 144, 76, 1781864420, 80, 2785129219, 68, 79, 69, 1, 77, 3834082981, 62, 144, 76, 1595404168, 77, 3820781604, 62, 144, 76, 2841528809, 77, 2869892222, 62, 199, 81, 6, 336357977, 5, 81, 75, 44757, 174, 17, 17, 75, 76, 802670612, 77, 3889194177, 62, 172, 75, 2772, 17, 17, 75, 76, 3407014352, 77, 2237611151, 62, 323, 81, 5, 13, 12, 3, 11, 1, 81, 194, 14, 54, 126073183, 75, 12517, 17, 17, 75, 76, 2400876820, 77, 2301044337, 62, 135, 136, 43, 9, 7, 8, 1, 82, 167, 75, 11594, 17, 17, 75, 76, 136423366, 77, 213698350, 62, 0, 81, 55, 1365998843, 137, 194, 13, 2, 3240182497, 75, 1842, 17, 17, 75, 76, 80657603, 77, 1665815084, 62, 172, 75, 35911, 17, 17, 75, 76, 1129841393, 77, 2817178699, 62, 0, 81, 4, 0, 138, 184, 81, 55, 1365998843, 82, 3, 81, 43, 10, 3, 82, 1, 5, 167, 75, 20593, 17, 17, 75, 76, 4198112724, 77, 419011751, 62, 172, 75, 8326, 17, 17, 75, 76, 927704233, 77, 2713616453, 62, 1, 75, 57054, 90, 91, 67, 75, 144, 76, 3522225225, 80, 3824797148, 68, 79, 69, 1, 77, 589731274, 62, 172, 75, 15808, 17, 17, 75, 76, 3475761429, 77, 2447353361, 62, 167, 75, 16563, 17, 17, 75, 76, 1993869641, 77, 1743810917, 62, 184, 81, 56, 1435138382, 82, 3, 81, 139, 34, 79, 81, 67, 144, 76, 3392820761, 80, 3695392519, 68, 83, 69, 1, 77, 3079068897, 62, 172, 75, 17629, 17, 17, 75, 76, 342379304, 77, 806288820, 62, 172, 75, 9251, 17, 17, 75, 76, 1174185687, 77, 4198292044, 62, 241, 3, 6, 81, 57, 2233651988, 4, 81, 167, 75, 21629, 17, 17, 75, 76, 2948872043, 77, 3724923009, 62, 1, 75, 12513, 90, 91, 67, 144, 76, 2420725722, 80, 2723296873, 68, 87, 69, 1, 77, 2432974941, 62, 167, 75, 18012, 17, 17, 75, 76, 2694914710, 77, 1283240058, 62, 167, 75, 2495, 17, 17, 75, 76, 3677859307, 77, 2546038551, 62, 258, 81, 4, 9, 4, 81, 75, 11968, 17, 17, 75, 76, 2522798816, 1, 77, 727211509, 62, 152, 81, 58, 1778635172, 82, 5, 81, 5, 82, 172, 75, 29502, 17, 17, 75, 76, 4027942409, 77, 112055842, 62, 203, 7, 59, 3450593780, 81, 60, 123013161, 8, 7, 81, 167, 75, 4175, 17, 17, 75, 76, 2816261713, 77, 3930739581, 62, 4, 2, 2, 81, 61, 773884956, 42, 6, 81, 1, 0, 167, 75, 17332, 17, 17, 75, 76, 4024224456, 77, 3931111201, 62, 45, 4, 42, 76, 78, 2, 77, 76, 140, 47, 78, 3655, 3, 11, 0, 0, 309, 20, 21, 62, 4158739647, 22, 21, 180, 23, 6, 336357977, 24, 23, 0, 25, 63, 3859162694, 141, 34, 79, 26, 70, 76, 144, 75, 2457501317, 80, 472840059, 71, 80, 72, 144, 76, 11055, 77, 13440, 63, 285, 27, 64, 2267923380, 24, 27, 28, 22, 24, 79, 28, 70, 144, 75, 892776950, 80, 2499642030, 71, 13, 80, 79, 80, 72, 144, 76, 29291, 77, 22220, 63, 148, 75, 4194782856, 76, 52234, 77, 29746, 63, 156, 29, 65, 1363422831, 30, 24, 29, 24, 30, 31, 22, 24, 79, 31, 70, 76, 144, 75, 1018700528, 80, 2625565608, 71, 80, 72, 144, 76, 56091, 77, 6148, 63, 148, 75, 3078880025, 76, 15472, 77, 49610, 63, 155, 32, 66, 4028795507, 33, 24, 32, 24, 33, 34, 22, 24, 79, 34, 70, 76, 144, 75, 3020157796, 80, 332055580, 71, 16, 80, 79, 80, 72, 144, 76, 9924, 77, 9551, 63, 148, 75, 3906216324, 76, 19704, 77, 20457, 63, 155, 35, 67, 4146887482, 36, 24, 35, 24, 36, 37, 22, 24, 79, 37, 70, 76, 144, 75, 1866407590, 80, 3503253918, 71, 16, 80, 79, 80, 72, 144, 76, 50755, 77, 47008, 63, 148, 75, 2078624989, 76, 55060, 77, 7814, 63, 156, 38, 68, 1448729088, 39, 24, 38, 24, 39, 40, 22, 24, 79, 40, 70, 144, 75, 3703544712, 80, 1015442496, 71, 13, 80, 80, 79, 72, 144, 76, 20857, 77, 11413, 63, 148, 75, 961815818, 76, 50098, 77, 306, 63, 156, 41, 69, 1633127308, 42, 24, 41, 24, 42, 43, 22, 24, 79, 43, 70, 76, 144, 75, 3315741527, 80, 627639311, 71, 80, 72, 144, 76, 37716, 77, 3180, 63, 148, 75, 2023881546, 76, 10080, 77, 55309, 63, 156, 44, 70, 695387599, 45, 24, 44, 24, 45, 46, 22, 24, 79, 46, 70, 76, 144, 75, 2558430212, 80, 4165295292, 71, 16, 80, 79, 80, 72, 144, 76, 28100, 77, 46848, 63, 148, 75, 2667219649, 76, 55682, 77, 59154, 63, 156, 47, 71, 2732671332, 48, 24, 47, 24, 48, 49, 22, 24, 79, 49, 70, 76, 144, 75, 1536061631, 80, 3172907959, 71, 80, 72, 144, 76, 25267, 77, 48298, 63, 148, 75, 3015758153, 76, 26285, 77, 35020, 63, 155, 50, 72, 350423615, 51, 24, 50, 24, 51, 52, 22, 24, 79, 52, 70, 144, 75, 1931030602, 80, 3537895682, 71, 13, 80, 79, 80, 72, 144, 76, 43840, 77, 34708, 63, 148, 75, 359919549, 76, 44224, 77, 31173, 63, 156, 53, 73, 4018684582, 54, 24, 53, 24, 54, 55, 22, 24, 79, 55, 70, 144, 75, 3522381649, 80, 834279433, 71, 13, 80, 80, 79, 72, 144, 76, 14609, 77, 576, 63, 148, 75, 842780337, 76, 20079, 77, 53112, 63, 156, 56, 74, 2946864334, 57, 24, 56, 24, 57, 58, 22, 24, 79, 58, 70, 76, 144, 75, 1037741924, 80, 2644607004, 71, 80, 72, 144, 76, 43688, 77, 24338, 63, 148, 75, 590890961, 76, 10989, 77, 46587, 63, 155, 59, 75, 444926581, 60, 24, 59, 24, 60, 61, 22, 24, 79, 61, 70, 76, 144, 75, 489571965, 80, 2096437045, 71, 80, 72, 144, 76, 60729, 77, 18552, 63, 148, 75, 1638618947, 76, 54579, 77, 56948, 63, 156, 62, 76, 3989679098, 63, 24, 62, 24, 63, 64, 22, 24, 79, 64, 70, 76, 144, 75, 692200718, 80, 1729422086, 71, 80, 72, 144, 76, 59474, 77, 1310, 63, 148, 75, 1126414438, 76, 56755, 77, 30364, 63, 155, 65, 77, 4189976121, 66, 24, 65, 24, 66, 67, 22, 24, 79, 67, 70, 144, 75, 534121836, 80, 2140986916, 71, 13, 80, 80, 79, 72, 144, 76, 31500, 77, 52546, 63, 148, 75, 2369381669, 76, 60348, 77, 51500, 63, 155, 68, 78, 3508035427, 69, 24, 68, 24, 69, 70, 22, 24, 79, 70, 70, 144, 75, 1769970727, 80, 3376835807, 71, 13, 80, 79, 80, 72, 144, 76, 31273, 77, 25668, 63, 148, 75, 1627086157, 76, 47908, 77, 15619, 63, 156, 71, 79, 2410019906, 72, 24, 71, 24, 72, 73, 22, 24, 79, 73, 70, 76, 144, 75, 1819265314, 80, 3426130394, 71, 80, 72, 144, 76, 9944, 77, 10766, 63, 148, 75, 2425453368, 76, 20948, 77, 59550, 63, 148, 75, 2861869823, 76, 22349, 77, 58838, 63, 309, 5, 81, 57, 2233651988, 6, 81, 181, 74, 9323, 22, 22, 74, 75, 874115326, 76, 4430, 77, 23633, 63, 8, 81, 0, 16, 323, 82, 2, 7, 17, 0, 81, 1, 82, 181, 74, 51817, 22, 22, 74, 75, 1858825579, 76, 38113, 77, 8607, 63, 195, 7, 19, 74, 13220, 22, 22, 74, 75, 1154923953, 76, 17563, 1, 77, 19348, 63, 212, 81, 6, 336357977, 7, 81, 74, 15143, 22, 22, 74, 75, 1179546907, 144, 76, 36145, 77, 18972, 63, 45, 6, 142, 171, 1, 81, 11, 80, 1103524790, 181, 74, 16750, 22, 22, 74, 75, 3511892137, 76, 51619, 77, 48919, 63, 204, 81, 60, 123013161, 15, 14, 81, 16, 55, 1365998843, 181, 74, 908, 22, 22, 74, 75, 1244897095, 76, 25349, 77, 17975, 63, 242, 81, 7, 82, 2, 3240182497, 19, 7, 82, 181, 74, 23639, 22, 22, 74, 75, 1970724889, 76, 8755, 77, 6900, 63, 243, 81, 81, 3767159064, 9, 1, 81, 10, 6, 336357977, 185, 74, 58217, 22, 22, 74, 75, 2627968259, 76, 25929, 77, 62407, 63, 193, 6, 18, 74, 16233, 22, 22, 74, 75, 443588332, 76, 25440, 1, 77, 30202, 63, 143, 43, 82, 14, 15, 1, 81, 165, 18, 6, 82, 74, 23196, 186, 22, 22, 74, 75, 2936573649, 76, 29563, 77, 57698, 63, 209, 81, 4, 0, 81, 22, 81, 7, 3, 34, 79, 81, 70, 76, 144, 75, 4294868704, 80, 4232912867, 71, 16, 80, 79, 80, 72, 144, 76, 39749, 77, 43772, 63, 185, 74, 19026, 22, 22, 74, 75, 3029033277, 76, 41231, 77, 56287, 63, 181, 74, 10443, 22, 22, 74, 75, 3818473921, 76, 47243, 77, 44241, 63, 16, 81, 12, 13, 210, 8, 81, 14, 59, 3450593780, 185, 74, 17073, 22, 22, 74, 75, 2547938513, 76, 36219, 77, 63628, 63, 205, 81, 1, 11, 12, 1, 81, 13, 82, 3312629066, 185, 74, 2058, 22, 22, 74, 75, 1789723969, 76, 64779, 77, 9661, 63, 181, 74, 17876, 22, 22, 74, 75, 3329703910, 76, 49766, 77, 51699, 63, 42, 75, 78, 3, 75, 76, 77, 58, 75, 207, 3, 4, 83, 4234643535, 189, 5, 0, 4, 6, 6, 336357977, 7, 5, 6, 0, 7, 261, 4, 0, 1, 5, 6, 336357977, 6, 4, 5, 0, 6, 7, 84, 3827338066, 234, 4, 85, 1379621844, 5, 7, 4, 6, 86, 2296539774, 8, 7, 5, 2, 0, 6, 220, 4, 6, 336357977, 5, 8, 4, 0, 5, 6, 87, 3853227102, 7, 0, 6, 189, 4, 0, 7, 5, 6, 336357977, 6, 4, 5, 0, 6, 265, 4, 0, 5, 6, 336357977, 6, 4, 5, 0, 6, 7, 88, 1642615857, 270, 4, 0, 7, 5, 89, 3188956906, 6, 0, 5, 8, 4, 6, 9, 6, 336357977, 274, 4, 8, 9, 0, 4, 5, 90, 2843108175, 6, 0, 5, 7, 6, 336357977, 178, 4, 6, 7, 0, 4, 5, 34, 1463730514, 49, 4, 1, 5, 0, 45, 4, 209, 4, 4, 0, 4, 4, 3, 265, 4, 1, 5, 6, 336357977, 6, 4, 5, 1, 6, 7, 91, 7961734, 224, 4, 1, 7, 5, 1, 4, 6, 6, 336357977, 8, 5, 6, 1, 8, 269, 4, 92, 2264838170, 5, 1, 4, 6, 93, 3756669865, 7, 1, 6, 8, 5, 7, 213, 4, 6, 336357977, 5, 8, 4, 1, 5, 6, 1, 0, 7, 6, 336357977, 263, 4, 6, 7, 1, 4, 5, 1, 8, 6, 336357977, 9, 5, 8, 218, 1, 9, 4, 84, 3827338066, 5, 85, 1379621844, 6, 4, 5, 7, 94, 4011793591, 43, 5, 4, 6, 2, 1, 7, 301, 4, 6, 336357977, 6, 5, 4, 1, 6, 1, 209, 4, 4, 0, 4, 207, 4, 6, 87, 3853227102, 19, 7, 2, 6, 278, 6, 1, 7, 8, 6, 336357977, 9, 6, 8, 5, 9, 10, 0, 5, 279, 6, 6, 336357977, 7, 10, 6, 0, 7, 8, 95, 937198509, 9, 0, 8, 273, 6, 6, 336357977, 7, 9, 6, 0, 7, 8, 96, 2052199781, 10, 0, 8, 216, 6, 6, 336357977, 7, 10, 6, 0, 7, 8, 84, 3827338066, 9, 85, 1379621844, 248, 6, 8, 9, 7, 97, 3694104382, 10, 8, 6, 2, 0, 7, 220, 6, 6, 336357977, 7, 10, 6, 0, 7, 8, 98, 2987909587, 9, 0, 8, 225, 6, 0, 9, 7, 6, 336357977, 8, 6, 7, 0, 8, 10, 99, 485767931, 225, 6, 0, 10, 7, 6, 336357977, 8, 6, 7, 0, 8, 9, 100, 3664059740, 305, 6, 0, 9, 7, 6, 336357977, 8, 6, 7, 0, 8, 45, 0, 209, 6, 4, 0, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 17; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // JUMP_1343 (specialized) + regs[fp + SLOTS.PC] = 1343; + break; + } + case 62: + { + // JUMP_2380 (specialized) + regs[fp + SLOTS.PC] = 2380; + break; + } + case 63: + { + // JUMP_3392 (specialized) + regs[fp + SLOTS.PC] = 3392; + break; + } + case 64: + { + // UNARY_POS_102_102 (specialized) + var dst = 102; + regs[base + dst] = +regs[base + 102]; + break; + } + case 65: + { + // SUB_103_103_98 (specialized) + var dst = 103; + var a = regs[base + 103]; + regs[base + dst] = a - regs[base + 98]; + break; + } + case 66: + { + // ADD_98_98_103 (specialized) + var dst = 98; + var a = regs[base + 98]; + regs[base + dst] = a + regs[base + 103]; + break; + } + case 67: + { + // UNARY_POS_79_79 (specialized) + var dst = 79; + regs[base + dst] = +regs[base + 79]; + break; + } + case 68: + { + // SUB_80_80_76 (specialized) + var dst = 80; + var a = regs[base + 80]; + regs[base + dst] = a - regs[base + 76]; + break; + } + case 69: + { + // ADD_76_76_80 (specialized) + var dst = 76; + var a = regs[base + 76]; + regs[base + dst] = a + regs[base + 80]; + break; + } + case 70: + { + // UNARY_POS_79_79 (specialized) + var dst = 79; + regs[base + dst] = +regs[base + 79]; + break; + } + case 71: + { + // SUB_80_80_75 (specialized) + var dst = 80; + var a = regs[base + 80]; + regs[base + dst] = a - regs[base + 75]; + break; + } + case 72: + { + // ADD_75_75_80 (specialized) + var dst = 75; + var a = regs[base + 75]; + regs[base + dst] = a + regs[base + 80]; + break; + } + case 73: + { + // UNARY_NEG_102_102 (specialized) + var dst = 102; + regs[base + dst] = -regs[base + 102]; + break; + } + case 74: + { + // ADD_21_21_97 (specialized) + var dst = 21; + var a = regs[base + 21]; + regs[base + dst] = a + regs[base + 97]; + break; + } + case 75: + { + // UNARY_NEG_79_79 (specialized) + var dst = 79; + regs[base + dst] = -regs[base + 79]; + break; + } + case 76: + { + // UNARY_NEG_79_79 (specialized) + var dst = 79; + regs[base + dst] = -regs[base + 79]; + break; + } + case 77: + { + // SUB_21_21_97 (specialized) + var dst = 21; + var a = regs[base + 21]; + regs[base + dst] = a - regs[base + 97]; + break; + } + case 78: + { + // BAND_103_102_103 (specialized) + var dst = 103; + var a = regs[base + 102]; + regs[base + dst] = a & regs[base + 103]; + break; + } + case 79: + { + // BAND_80_79_80 (specialized) + var dst = 80; + var a = regs[base + 79]; + regs[base + dst] = a & regs[base + 80]; + break; + } + case 80: + { + // BAND_80_80_79 (specialized) + var dst = 80; + var a = regs[base + 80]; + regs[base + dst] = a & regs[base + 79]; + break; + } + case 81: + { + // MUL_103_102_103 (specialized) + var dst = 103; + var a = regs[base + 102]; + regs[base + dst] = a * regs[base + 103]; + break; + } + case 82: + { + // BAND_103_103_102 (specialized) + var dst = 103; + var a = regs[base + 103]; + regs[base + dst] = a & regs[base + 102]; + break; + } + case 83: + { + // MUL_80_80_79 (specialized) + var dst = 80; + var a = regs[base + 80]; + regs[base + dst] = a * regs[base + 79]; + break; + } + case 84: + { + // MUL_103_103_102 (specialized) + var dst = 103; + var a = regs[base + 103]; + regs[base + dst] = a * regs[base + 102]; + break; + } + case 85: + { + // LT_97_21_97 (specialized) + var dst = 97; + var a = regs[base + 21]; + regs[base + dst] = a < regs[base + 97]; + break; + } + case 86: + { + // UNARY_NOT_102_97 (specialized) + var dst = 102; + regs[base + dst] = !regs[base + 97]; + break; + } + case 87: + { + // MUL_80_79_80 (specialized) + var dst = 80; + var a = regs[base + 79]; + regs[base + dst] = a * regs[base + 80]; + break; + } + case 88: + { + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 89: + { + // LOAD_INT_18_8 (specialized) + var dst = 18; + regs[base + dst] = 8; + break; + } + case 90: + { + // LT_75_17_75 (specialized) + var dst = 75; + var a = regs[base + 17]; + regs[base + dst] = a < regs[base + 75]; + break; + } + case 91: + { + // UNARY_NOT_79_75 (specialized) + var dst = 79; + regs[base + dst] = !regs[base + 75]; + break; + } + case 92: + { + // LOAD_CONST_7_0_3437391080 (specialized) + var dst = 7; + regs[base + dst] = this._constant(0, 3437391080); + break; + } + case 93: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 94: + { + // MOVE_2_12 (specialized) + var dst = 2; + regs[base + dst] = regs[base + 12]; + break; + } + case 95: + { + // LOAD_GLOBAL_12_1_4164083900 (specialized) + var dst = 12; + var globalName = this._constant(1, 4164083900); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 96: + { + // LOAD_INT_9_3487949257 (specialized) + var dst = 9; + regs[base + dst] = 3487949257; + break; + } + case 97: + { + // LOAD_INT_10_186 (specialized) + var dst = 10; + regs[base + dst] = 186; + break; + } + case 98: + { + // LOAD_INT_11_11 (specialized) + var dst = 11; + regs[base + dst] = 11; + break; + } + case 99: + { + // LOAD_CONST_13_2_3240182497 (specialized) + var dst = 13; + regs[base + dst] = this._constant(2, 3240182497); + break; + } + case 100: + { + // UNARY_NEG_14_13 (specialized) + var dst = 14; + regs[base + dst] = -regs[base + 13]; + break; + } + case 101: + { + // LOAD_CONST_14_3_4193449357 (specialized) + var dst = 14; + regs[base + dst] = this._constant(3, 4193449357); + break; + } + case 102: + { + // LOAD_GLOBAL_14_1_4164083900 (specialized) + var dst = 14; + var globalName = this._constant(1, 4164083900); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 103: + { + // GET_PROP_16_14_4 (specialized) + // dst = regs[obj][regs[key]] + var dst = 16; + var obj = regs[base + 14]; + var key = regs[base + 4]; + regs[base + dst] = obj[key]; + break; + } + case 104: + { + // SET_PROP_12_3_14 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 12]; + var key = regs[base + 3]; + var val = regs[base + 14]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 105: + { + // LOAD_CONST_12_4_0 (specialized) + var dst = 12; + regs[base + dst] = this._constant(4, 0); + break; + } + case 106: + { + // RETURN_12 (specialized) + var retVal = regs[base + 12]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 107: + { + // LOAD_THIS_19 (specialized) + var dst = 19; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 108: + { + // MOVE_21_20 (specialized) + var dst = 21; + regs[base + dst] = regs[base + 20]; + break; + } + case 109: + { + // MOVE_23_22 (specialized) + var dst = 23; + regs[base + dst] = regs[base + 22]; + break; + } + case 110: + { + // NEQ_25_21_24 (specialized) + var dst = 25; + var a = regs[base + 21]; + regs[base + dst] = a !== regs[base + 24]; + break; + } + case 111: + { + // UNARY_NOT_102_25 (specialized) + var dst = 102; + regs[base + dst] = !regs[base + 25]; + break; + } + case 112: + { + // MOVE_23_26 (specialized) + var dst = 23; + regs[base + dst] = regs[base + 26]; + break; + } + case 113: + { + // EQ_27_21_23 (specialized) + var dst = 27; + var a = regs[base + 21]; + regs[base + dst] = a === regs[base + 23]; + break; + } + case 114: + { + // UNARY_NOT_102_27 (specialized) + var dst = 102; + regs[base + dst] = !regs[base + 27]; + break; + } + case 115: + { + // EQ_30_21_23 (specialized) + var dst = 30; + var a = regs[base + 21]; + regs[base + dst] = a === regs[base + 23]; + break; + } + case 116: + { + // EQ_33_21_23 (specialized) + var dst = 33; + var a = regs[base + 21]; + regs[base + dst] = a === regs[base + 23]; + break; + } + case 117: + { + // EQ_36_21_23 (specialized) + var dst = 36; + var a = regs[base + 21]; + regs[base + dst] = a === regs[base + 23]; + break; + } + case 118: + { + // LOAD_GLOBAL_7_32_1012622967 (specialized) + var dst = 7; + var globalName = this._constant(32, 1012622967); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 119: + { + // LOAD_GLOBAL_11_1_4164083900 (specialized) + var dst = 11; + var globalName = this._constant(1, 4164083900); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 120: + { + // LOAD_UPVALUE_14_0 (specialized) + var dst = 14; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 121: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 122: + { + // SET_PROP_11_12_13 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 11]; + var key = regs[base + 12]; + var val = regs[base + 13]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 123: + { + // JUMP_REG_4 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 4]; + break; + } + case 124: + { + // GET_PROP_104_3_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 104; + var obj = regs[base + 3]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; + break; + } + case 125: + { + // LT_104_0_6 (specialized) + var dst = 104; + var a = regs[base + 0]; + regs[base + dst] = a < regs[base + 6]; + break; + } + case 126: + { + // THROW_5 (specialized) + throw regs[base + 5]; + break; + } + case 127: + { + // THROW_104 (specialized) + throw regs[base + 104]; + break; + } + case 128: + { + // FINALLY_SETUP_937_4_5_1188 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 937, + contReg: 4, + payloadReg: 5, + throwPad: 1188 + }); + break; + } + case 129: + { + // TRY_SETUP_1232_3 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 1232, + exceptionReg: 3 + }); + break; + } + case 130: + { + // RETURN_104 (specialized) + var retVal = regs[base + 104]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 131: + { + // GET_PROP_98_98_101 (specialized) + // dst = regs[obj][regs[key]] + var dst = 98; + var obj = regs[base + 98]; + var key = regs[base + 101]; + regs[base + dst] = obj[key]; + break; + } + case 132: + { + // JUMP_REG_98 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 98]; + break; + } + case 133: + { + // LOAD_THIS_15 (specialized) + var dst = 15; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 134: + { + // NEQ_21_17_20 (specialized) + var dst = 21; + var a = regs[base + 17]; + regs[base + dst] = a !== regs[base + 20]; + break; + } + case 135: + { + // SHL_81_12_14 (specialized) + var dst = 81; + var a = regs[base + 12]; + regs[base + dst] = a << regs[base + 14]; + break; + } + case 136: + { + // BOR_82_10_81 (specialized) + var dst = 82; + var a = regs[base + 10]; + regs[base + dst] = a | regs[base + 81]; + break; + } + case 137: + { + // GET_PROP_11_3_81 (specialized) + // dst = regs[obj][regs[key]] + var dst = 11; + var obj = regs[base + 3]; + var key = regs[base + 81]; + regs[base + dst] = obj[key]; + break; + } + case 138: + { + // RETURN_81 (specialized) + var retVal = regs[base + 81]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 139: + { + // LT_81_5_82 (specialized) + var dst = 81; + var a = regs[base + 5]; + regs[base + dst] = a < regs[base + 82]; + break; + } + case 140: + { + // JUMP_REG_76 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 76]; + break; + } + case 141: + { + // NEQ_26_22_25 (specialized) + var dst = 26; + var a = regs[base + 22]; + regs[base + dst] = a !== regs[base + 25]; + break; + } + case 142: + { + // BOR_81_9_10 (specialized) + var dst = 81; + var a = regs[base + 9]; + regs[base + dst] = a | regs[base + 10]; + break; + } + case 143: + { + // BXOR_81_17_8 (specialized) + var dst = 81; + var a = regs[base + 17]; + regs[base + dst] = a ^ regs[base + 8]; + break; + } + case 144: + { + // LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 145: + { + // MOVE,EQ,UNARY_NOT (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + // UNARY_NOT + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case 146: + { + // MOVE,EQ (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case 147: + { + // EQ,UNARY_NOT (macro) + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + // UNARY_NOT + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case 148: + { + // LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 149: + { + // LOAD_CONST,ADD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 150: + { + // LOAD_CONST,SUB (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case 151: + { + // ADD,MOVE (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 152: + { + // LOAD_CONST,ADD,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 153: + { + // LOAD_CONST,SUB,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 154: + { + // SUB,MOVE (macro) + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 155: + { + // LOAD_CONST,SUB,MOVE,EQ,UNARY_NOT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + // UNARY_NOT + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case 156: + { + // LOAD_CONST,ADD,MOVE,EQ,UNARY_NOT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + // UNARY_NOT + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case 157: + { + // LOAD_CONST,SUB,MOVE,EQ (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case 158: + { + // SUB,MOVE,EQ,UNARY_NOT (macro) + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + // UNARY_NOT + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case 159: + { + // LOAD_CONST,ADD,MOVE,EQ (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case 160: + { + // ADD,MOVE,EQ,UNARY_NOT (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + // UNARY_NOT + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case 161: + { + // SUB,MOVE,EQ (macro) + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case 162: + { + // ADD,MOVE,EQ (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case 163: + { + // LOAD_CONST,USHR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 164: + { + // LOAD_CONST,USHR,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 165: + { + // ADD,LOAD_INT (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 166: + { + // USHR,MOVE (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 167: + { + // LOAD_INT,ADD,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 168: + { + // LOAD_INT,ADD,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 169: + { + // ADD,LOAD_INT,LOAD_INT (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 170: + { + // LOAD_INT,ADD (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 171: + { + // MOVE,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 172: + { + // LOAD_INT,SUB,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 173: + { + // LOAD_INT,SUB,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 174: + { + // SUB,LOAD_INT,LOAD_INT (macro) + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 175: + { + // LOAD_INT,SUB (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case 176: + { + // SUB,LOAD_INT (macro) + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 177: + { + // LOAD_CONST,USHR,MOVE,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 178: + { + // USHR,MOVE,LOAD_CONST (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 179: + { + // MOVE,LOAD_INT (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 180: + { + // LOAD_CONST,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 181: + { + // LOAD_INT,ADD,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 182: + { + // ADD,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 183: + { + // LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 184: + { + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 185: + { + // LOAD_INT,SUB,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 186: + { + // SUB,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 187: + { + // ADD,LOAD_CONST (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 188: + { + // BXOR,LOAD_CONST (macro) + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 189: + { + // BXOR,LOAD_CONST,USHR,MOVE (macro) + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 190: + { + // ADD,LOAD_CONST,USHR,MOVE (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 191: + { + // BXOR,LOAD_CONST,USHR (macro) + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 192: + { + // ADD,LOAD_CONST,USHR (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 193: + { + // MOVE,LOAD_INT,SUB,LOAD_INT,LOAD_INT (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 194: + { + // LOAD_CONST,LOAD_INT,ADD,LOAD_INT,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 195: + { + // MOVE,LOAD_INT,ADD,LOAD_INT,LOAD_INT (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 196: + { + // MOVE,LOAD_INT,SUB,LOAD_INT (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 197: + { + // LOAD_CONST,LOAD_INT,ADD,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 198: + { + // MOVE,LOAD_INT,ADD,LOAD_INT (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 199: + { + // LOAD_CONST,MOVE,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 200: + { + // MOVE,LOAD_INT,SUB (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case 201: + { + // LOAD_CONST,LOAD_INT,ADD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 202: + { + // MOVE,LOAD_INT,ADD (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 203: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 204: + { + // LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 205: + { + // USHR,BXOR,LOAD_CONST (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 206: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 207: + { + // LOAD_THIS,LOAD_CONST (macro) + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 208: + { + // GET_PROP,LOAD_CONST (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 209: + { + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 210: + { + // MOVE,LOAD_GLOBAL (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 211: + { + // USHR,BXOR (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case 212: + { + // LOAD_CONST,MOVE,LOAD_INT,ADD,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 213: + { + // LOAD_CONST,USHR,MOVE,ADD,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 214: + { + // USHR,MOVE,ADD,LOAD_CONST,USHR (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 215: + { + // MOVE,ADD,LOAD_CONST,USHR,MOVE (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 216: + { + // LOAD_CONST,USHR,MOVE,LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 217: + { + // USHR,MOVE,LOAD_GLOBAL,LOAD_CONST,GET_PROP (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 218: + { + // MOVE,LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 219: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 220: + { + // LOAD_CONST,USHR,MOVE,LOAD_CONST,USHR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 221: + { + // USHR,MOVE,LOAD_CONST,USHR,BXOR (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case 222: + { + // MOVE,LOAD_CONST,USHR,BXOR,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 223: + { + // LOAD_CONST,USHR,BXOR,LOAD_CONST,USHR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 224: + { + // USHR,BXOR,LOAD_CONST,USHR,MOVE (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 225: + { + // BXOR,LOAD_CONST,USHR,MOVE,LOAD_CONST (macro) + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 226: + { + // LOAD_CONST,MOVE,LOAD_INT,ADD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 227: + { + // LOAD_CONST,USHR,MOVE,ADD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 228: + { + // USHR,MOVE,ADD,LOAD_CONST (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 229: + { + // MOVE,ADD,LOAD_CONST,USHR (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 230: + { + // LOAD_CONST,USHR,MOVE,LOAD_GLOBAL (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 231: + { + // USHR,MOVE,LOAD_GLOBAL,LOAD_CONST (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 232: + { + // MOVE,LOAD_GLOBAL,LOAD_CONST,GET_PROP (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 233: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 234: + { + // LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 235: + { + // USHR,MOVE,LOAD_CONST,USHR (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 236: + { + // MOVE,LOAD_CONST,USHR,BXOR (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case 237: + { + // LOAD_CONST,USHR,BXOR,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 238: + { + // USHR,BXOR,LOAD_CONST,USHR (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 239: + { + // UNARY_BITNOT,LOAD_CONST,USHR,MOVE (macro) + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 240: + { + // BOR,LOAD_CONST,USHR,MOVE (macro) + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 241: + { + // MOVE,LOAD_CONST,MOVE (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 242: + { + // MOVE,LOAD_CONST,ADD (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 243: + { + // LOAD_CONST,ADD,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 244: + { + // USHR,MOVE,ADD (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 245: + { + // MOVE,ADD,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 246: + { + // USHR,MOVE,LOAD_GLOBAL (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 247: + { + // MOVE,LOAD_GLOBAL,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 248: + { + // GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 249: + { + // MOVE,LOAD_CONST,USHR (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 250: + { + // LOAD_CONST,USHR,BXOR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case 251: + { + // UNARY_BITNOT,LOAD_CONST,USHR (macro) + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 252: + { + // BOR,LOAD_CONST,USHR (macro) + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 253: + { + // MOVE,ADD (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 254: + { + // LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 255: + { + // UNARY_BITNOT,LOAD_CONST (macro) + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 256: + { + // LOAD_CONST,SHL (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case 257: + { + // BOR,LOAD_CONST (macro) + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 258: + { + // ADD,MOVE,LOAD_INT,SUB,LOAD_INT (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 259: + { + // LOAD_CONST,LOAD_INT,SUB,LOAD_INT,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 260: + { + // LOAD_CONST,BXOR,LOAD_CONST,USHR,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 261: + { + // ADD,LOAD_CONST,USHR,MOVE,LOAD_GLOBAL (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 262: + { + // LOAD_CONST,USHR,MOVE,UNARY_BITNOT,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 263: + { + // USHR,MOVE,UNARY_BITNOT,LOAD_CONST,USHR (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 264: + { + // MOVE,UNARY_BITNOT,LOAD_CONST,USHR,MOVE (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 265: + { + // UNARY_BITNOT,LOAD_CONST,USHR,MOVE,LOAD_CONST (macro) + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 266: + { + // LOAD_CONST,USHR,MOVE,LOAD_CONST,SHL (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case 267: + { + // USHR,MOVE,LOAD_CONST,SHL,LOAD_CONST (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 268: + { + // MOVE,LOAD_CONST,SHL,LOAD_CONST,USHR (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 269: + { + // LOAD_CONST,SHL,LOAD_CONST,USHR,BOR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case 270: + { + // SHL,LOAD_CONST,USHR,BOR,LOAD_CONST (macro) + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 271: + { + // LOAD_CONST,USHR,BOR,LOAD_CONST,USHR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 272: + { + // USHR,BOR,LOAD_CONST,USHR,MOVE (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 273: + { + // LOAD_CONST,USHR,MOVE,LOAD_CONST,ADD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 274: + { + // USHR,MOVE,LOAD_CONST,ADD,LOAD_CONST (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 275: + { + // MOVE,LOAD_CONST,ADD,LOAD_CONST,USHR (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 276: + { + // LOAD_CONST,ADD,LOAD_CONST,USHR,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 277: + { + // ADD,LOAD_CONST,USHR,MOVE,LOAD_CONST (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 278: + { + // BOR,LOAD_CONST,USHR,MOVE,ADD (macro) + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 279: + { + // LOAD_CONST,USHR,MOVE,LOAD_CONST,SUB (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case 280: + { + // USHR,MOVE,LOAD_CONST,SUB,LOAD_CONST (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 281: + { + // MOVE,LOAD_CONST,SUB,LOAD_CONST,USHR (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 282: + { + // LOAD_CONST,SUB,LOAD_CONST,USHR,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 283: + { + // LOAD_INT,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 284: + { + // LOAD_CONST,MOVE,LOAD_CONST,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 285: + { + // LOAD_CONST,MOVE,EQ,UNARY_NOT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + // UNARY_NOT + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case 286: + { + // ADD,MOVE,LOAD_INT,SUB (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case 287: + { + // LOAD_CONST,LOAD_INT,SUB,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 288: + { + // LOAD_CONST,BXOR,LOAD_CONST,USHR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 289: + { + // LOAD_CONST,USHR,MOVE,UNARY_BITNOT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case 290: + { + // USHR,MOVE,UNARY_BITNOT,LOAD_CONST (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 291: + { + // MOVE,UNARY_BITNOT,LOAD_CONST,USHR (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 292: + { + // USHR,MOVE,LOAD_CONST,SHL (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case 293: + { + // MOVE,LOAD_CONST,SHL,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 294: + { + // LOAD_CONST,SHL,LOAD_CONST,USHR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 295: + { + // SHL,LOAD_CONST,USHR,BOR (macro) + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case 296: + { + // LOAD_CONST,USHR,BOR,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 297: + { + // USHR,BOR,LOAD_CONST,USHR (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 298: + { + // USHR,MOVE,LOAD_CONST,ADD (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 299: + { + // MOVE,LOAD_CONST,ADD,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 300: + { + // LOAD_CONST,ADD,LOAD_CONST,USHR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 301: + { + // LOAD_CONST,USHR,MOVE,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 302: + { + // USHR,MOVE,LOAD_CONST,SUB (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case 303: + { + // MOVE,LOAD_CONST,SUB,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 304: + { + // LOAD_CONST,SUB,LOAD_CONST,USHR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 305: + { + // SUB,LOAD_CONST,USHR,MOVE (macro) + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 306: + { + // LOAD_CONST,MOVE,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 307: + { + // LOAD_CONST,MOVE,EQ (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // EQ + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case 308: + { + // ADD,MOVE,LOAD_INT (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 309: + { + // LOAD_THIS,LOAD_CONST,MOVE (macro) + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 310: + { + // LOAD_CONST,LOAD_INT,SUB (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case 311: + { + // LOAD_CONST,BXOR,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 312: + { + // USHR,MOVE,UNARY_BITNOT (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case 313: + { + // MOVE,UNARY_BITNOT,LOAD_CONST (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 314: + { + // MOVE,LOAD_CONST,SHL (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case 315: + { + // LOAD_CONST,SHL,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 316: + { + // SHL,LOAD_CONST,USHR (macro) + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 317: + { + // LOAD_CONST,USHR,BOR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case 318: + { + // USHR,BOR,LOAD_CONST (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 319: + { + // USHR,MOVE,RETURN (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 320: + { + // MOVE,LOAD_CONST,SUB (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case 321: + { + // LOAD_CONST,SUB,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 322: + { + // SUB,LOAD_CONST,USHR (macro) + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + case 323: + { + // ADD,CALL_METHOD (macro) + // ADD + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 324: + { + // LOAD_CONST,BXOR (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BXOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case 325: + { + // MOVE,UNARY_BITNOT (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // UNARY_BITNOT + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case 326: + { + // SHL,LOAD_CONST (macro) + // SHL + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 327: + { + // USHR,BOR (macro) + // USHR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + // BOR + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case 328: + { + // MOVE,RETURN (macro) + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 329: + { + // SUB,LOAD_CONST (macro) + // SUB + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/qualification.json new file mode 100644 index 00000000..9e98618a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/qualification.json @@ -0,0 +1,77 @@ +{ + "id": "exceptions--stringConcealingControlFlowFlatteningDispatcherConcealConstantsSpecializedOpcodesMacroOpcodes--97", + "case": "exceptions", + "profile": "stringConcealingControlFlowFlatteningDispatcherConcealConstantsSpecializedOpcodesMacroOpcodes", + "seed": 97, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": true, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": null, + "components": { + "stringConcealing": null, + "controlFlowFlattening": null, + "dispatcher": null, + "concealConstants": null, + "specializedOpcodes": null, + "macroOpcodes": null + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/macro-six-option-no-anti-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-169.js b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-169.js new file mode 100644 index 00000000..fbaad333 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-169.js @@ -0,0 +1,16 @@ +function c(a,f,h){this.d=a;this.A=h;this.h=f;this.l=null;this.p=[];this.c=1;this.g=0}var B=new WeakMap;function fa(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})}function C(a,f){this.f="x";this.p=a;this.m=f;this.u=!1;this.o=void 0}function D(a,f,h){this.E=h;this.s=a;this.i=[];this.prototype={};this.r=f}C.prototype.e=function(a,f,h){this.u?this.o=h:this.p[this.m]=h};C.prototype.Q=function(){return this.u?this.o:this.p[this.m]}; +c.prototype.k=function(a,f,h){a=h??J(this);f=f??J(this);a=this.A[a];if(!f)return a;if(typeof a==="number")return a^f;if(typeof a!=="string")return a;a=fa(a);h="";for(var b=0;b>>13)&65535);return h};function ha(a,f,h){f=a.p[f+7]+h;h=a.l||(a.l=[]);return h[f]||(h[f]=new C(a.p,f))}function J(a){return a.d[a.p[a.g+0]++]} +function K(a,f,h,b,l,g){for(var m=l.s,d=a.p,e=a.c,u=8+m.z,x=e+u;d.length=a.d.length)break;b[g+0]=m+1;m=a.d[m];try{var d=b[g+7];switch(m){case 0:var e=J(a);b[d+e]=a.k();break;case 1:e=J(a);b[d+e]=J(a);break;case 2:e=J(a);var u=a.k();if(!(u in a.h))throw new ReferenceError(`${u} is not defined`);b[d+e]=a.h[u];break;case 3:e=J(a);b[d+e]=b[g+4].i[J(a)].Q(void 0,void 0,[],14);break;case 4:e=J(a);b[d+e]=b[g+3];break;case 5:e=J(a);b[d+e]=b[d+J(a)];break;case 6:a.h[a.k()]=b[d+J(a)];break;case 7:var x= +J(a);b[g+4].i[x].e(21,void 0,b[d+J(a)]);break;case 8:e=J(a);var n=b[d+J(a)],p=b[d+J(a)];b[d+e]=n[p];break;case 9:n=b[d+J(a)];p=b[d+J(a)];var y=b[d+J(a)];Reflect.set(n,p,y);break;case 10:e=J(a);n=b[d+J(a)];p=b[d+J(a)];b[d+e]=delete n[p];break;case 11:e=J(a);var k=b[d+J(a)];b[d+e]=k+b[d+J(a)];break;case 12:e=J(a);k=b[d+J(a)];b[d+e]=k-b[d+J(a)];break;case 13:e=J(a);k=b[d+J(a)];b[d+e]=k*b[d+J(a)];break;case 14:e=J(a);k=b[d+J(a)];b[d+e]=k/b[d+J(a)];break;case 15:e=J(a);k=b[d+J(a)];b[d+e]=k%b[d+J(a)];break; +case 60:e=J(a);k=b[d+J(a)];b[d+e]=Math.pow(k,b[d+J(a)]);break;case 16:e=J(a);k=b[d+J(a)];b[d+e]=k&b[d+J(a)];break;case 17:e=J(a);k=b[d+J(a)];b[d+e]=k|b[d+J(a)];break;case 18:e=J(a);k=b[d+J(a)];b[d+e]=k^b[d+J(a)];break;case 19:e=J(a);k=b[d+J(a)];b[d+e]=k<>b[d+J(a)];break;case 21:e=J(a);k=b[d+J(a)];b[d+e]=k>>>b[d+J(a)];break;case 22:e=J(a);k=b[d+J(a)];b[d+e]=kb[d+J(a)];break;case 24:e=J(a);k=b[d+ +J(a)];b[d+e]=k<=b[d+J(a)];break;case 25:e=J(a);k=b[d+J(a)];b[d+e]=k>=b[d+J(a)];break;case 26:e=J(a);k=b[d+J(a)];b[d+e]=k===b[d+J(a)];break;case 27:e=J(a);k=b[d+J(a)];b[d+e]=k!==b[d+J(a)];break;case 28:e=J(a);k=b[d+J(a)];b[d+e]=k==b[d+J(a)];break;case 29:e=J(a);k=b[d+J(a)];b[d+e]=k!=b[d+J(a)];break;case 30:e=J(a);k=b[d+J(a)];b[d+e]=k in b[d+J(a)];break;case 31:e=J(a);n=b[d+J(a)];b[d+e]=n instanceof b[d+J(a)];break;case 32:e=J(a);b[d+e]=-b[d+J(a)];break;case 33:e=J(a);b[d+e]=+b[d+J(a)];break;case 34:e= +J(a);b[d+e]=!b[d+J(a)];break;case 35:e=J(a);b[d+e]=~b[d+J(a)];break;case 36:e=J(a);b[d+e]=typeof b[d+J(a)];break;case 37:e=J(a);J(a);b[d+e]=void 0;break;case 38:e=J(a);var U=a.k();y=Object.prototype.hasOwnProperty.call(a.h,U)?a.h[U]:void 0;b[d+e]=typeof y;break;case 39:b[g+0]=J(a);break;case 40:var L=J(a),M=J(a);b[d+L]||(b[g+0]=M);break;case 41:L=J(a);M=J(a);b[d+L]&&(b[g+0]=M);break;case 42:e=J(a);var q=b[d+J(a)],r=J(a);if(r===65535)f=b[d+J(a)];else for(f=Array(r),l=0;l>1):0]=E;break;case 46:throw b[d+J(a)];case 47:e=J(a);var ja=J(a),ka=J(a),la=J(a),X=J(a),ma=J(a),O=Array(X);for(l=0;l=H.D.length?b[g+0]=sa:b[d+e]=H.D[H.v++];break;case 54:var v=b[g+5];v||(b[g+5]=v=[]);v.push({a:J(a),B:J(a)});break;case 55:b[g+5].pop();break;case 59:(v=b[g+5])||(b[g+5]=v=[]);v.push({b:J(a),J:J(a),I:J(a),H:J(a)});break;case 56:var da=J(a),ea=J(a),ta=J(a),I=J(a)^da|0;for(g=ea;g>>13)>>>0;break;case 58:b[g+0]=b[d+J(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+" at pc "+(b[g+0]- +1));}}catch(z){g=0;for(m=a.g;m;){var w=b[m+5];if(w&&w.length>0){g=m;break}R(a,m);a.c=m;m=b[m+1];a.g=m}if(!g)throw z;m=b[g+5].pop();w=b[g+7];m.B!==void 0?(b[w+m.B]=z,b[g+0]=m.a):(b[w+m.J]=m.H,b[w+m.I]=z,b[g+0]=m.b);a.c=g+b[g+6];a.g=g}if(!a.g)return a.p[0]}}var T=globalThis;typeof window!=="undefined"&&(T.window=window,T.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(T.module=module,T.exports=typeof exports!=="undefined"?exports:void 0); +S(new c([4,1,47,3,52,1,4,0,0,5,2,3,2,3,0,0,0,4,1,0,0,5,2,0,42,6,2,1,5,0,5,3,0,42,7,6,1,5,42,5,7,0,9,3,4,5,0,3,4,0,45,3,4,2,47,3,71,1,4,1,0,1,0,45,3,0,3,4,0,45,3,4,2,47,3,92,0,5,2,0,0,0,1,0,45,3,0,3,4,0,45,3,4,1,3,2,0,0,3,5,0,11,4,2,3,3,2,1,11,3,4,2,45,3,0,2,4,0,45,2],T,["window","TEST_OUTPUT","id",7,void 0,":"]),null,new D({j:0,z:8,C:0},null,[])); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-170.js b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-170.js new file mode 100644 index 00000000..f944cd76 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-170.js @@ -0,0 +1,17 @@ +function b(a,f,e,l,k){this.h=l;this.C=a;this.E=e;this.g=k;this.t=null;this.f=[];this.z=f;this.j=1;this.s=0}var A=new WeakMap;function B(a){this.G=a;this.i=[];this.prototype={}}function C(a,f){this.f=f;this.o=a;this.u=!1;this.q=void 0}function da(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})} +b.prototype.w=function(a){var f=this.t;if(f){var e=this.f,l=a+e[a+6];for(a=e[a+7];a>>13)&65535);return l}function ea(a,f){a.u?a.q=f:a.f[a.o]=f}function fa(a,f,e){a.x="q";f=a.f[f+7]+e;e=a.t||(a.t=[]);return e[f]||(e[f]=new C(f,a.f))} +b.prototype.e=function(a,f,e,l,k,p,n){var g;this.n=e;for(D(this,0,l,n,f);;){e=this.s;a=this.f;k=a[e+0];if(k>=this.h.length)break;a[e+0]=k+1;k=this.h[k];try{var c=a[e+7];switch(k){case 0:var d=E(this);a[c+d]=Q(this);break;case 1:d=E(this);a[c+d]=E(this);break;case 2:d=E(this);var q=Q(this);if(!(q in this.g))throw new ReferenceError(`${q} is not defined`);a[c+d]=this.g[q];break;case 3:d=E(this);a[c+d]=a[e+4].i[E(this)].r(void 0,null,"x");break;case 4:d=E(this);a[c+d]=a[e+3];break;case 5:d=E(this);a[c+ +d]=a[c+E(this)];break;case 6:this.g[Q(this)]=a[c+E(this)];break;case 7:var ha=E(this);ea(a[e+4].i[ha],a[c+E(this)]);break;case 8:d=E(this);var m=a[c+E(this)],r=a[c+E(this)];a[c+d]=m[r];break;case 9:m=a[c+E(this)];r=a[c+E(this)];var x=a[c+E(this)];Reflect.set(m,r,x);break;case 10:d=E(this);m=a[c+E(this)];r=a[c+E(this)];a[c+d]=delete m[r];break;case 11:d=E(this);var h=a[c+E(this)];a[c+d]=h+a[c+E(this)];break;case 12:d=E(this);h=a[c+E(this)];a[c+d]=h-a[c+E(this)];break;case 13:d=E(this);h=a[c+E(this)]; +a[c+d]=h*a[c+E(this)];break;case 14:d=E(this);h=a[c+E(this)];a[c+d]=h/a[c+E(this)];break;case 15:d=E(this);h=a[c+E(this)];a[c+d]=h%a[c+E(this)];break;case 60:d=E(this);h=a[c+E(this)];a[c+d]=Math.pow(h,a[c+E(this)]);break;case 16:d=E(this);h=a[c+E(this)];a[c+d]=h&a[c+E(this)];break;case 17:d=E(this);h=a[c+E(this)];a[c+d]=h|a[c+E(this)];break;case 18:d=E(this);h=a[c+E(this)];a[c+d]=h^a[c+E(this)];break;case 19:d=E(this);h=a[c+E(this)];a[c+d]=h<>a[c+E(this)];break;case 21:d=E(this);h=a[c+E(this)];a[c+d]=h>>>a[c+E(this)];break;case 22:d=E(this);h=a[c+E(this)];a[c+d]=ha[c+E(this)];break;case 24:d=E(this);h=a[c+E(this)];a[c+d]=h<=a[c+E(this)];break;case 25:d=E(this);h=a[c+E(this)];a[c+d]=h>=a[c+E(this)];break;case 26:d=E(this);h=a[c+E(this)];a[c+d]=h===a[c+E(this)];break;case 27:d=E(this);h=a[c+E(this)];a[c+d]=h!==a[c+E(this)];break;case 28:d=E(this);h=a[c+E(this)];a[c+d]=h== +a[c+E(this)];break;case 29:d=E(this);h=a[c+E(this)];a[c+d]=h!=a[c+E(this)];break;case 30:d=E(this);h=a[c+E(this)];a[c+d]=h in a[c+E(this)];break;case 31:d=E(this);m=a[c+E(this)];a[c+d]=m instanceof a[c+E(this)];break;case 32:d=E(this);a[c+d]=-a[c+E(this)];break;case 33:d=E(this);a[c+d]=+a[c+E(this)];break;case 34:d=E(this);a[c+d]=!a[c+E(this)];break;case 35:d=E(this);a[c+d]=~a[c+E(this)];break;case 36:d=E(this);a[c+d]=typeof a[c+E(this)];break;case 37:d=E(this);E(this);a[c+d]=void 0;break;case 38:d= +E(this);var S=Q(this);x=Object.prototype.hasOwnProperty.call(this.g,S)?this.g[S]:void 0;a[c+d]=typeof x;break;case 39:a[e+0]=E(this);break;case 40:var K=E(this),L=E(this);a[c+K]||(a[e+0]=L);break;case 41:K=E(this);L=E(this);a[c+K]&&(a[e+0]=L);break;case 42:d=E(this);var t=a[c+E(this)],u=E(this);if(u===65535)f=a[c+E(this)];else for(f=Array(u),g=0;g>1):0]=F;break;case 46:throw a[c+E(this)];case 47:d=E(this);var ja=E(this),ka=E(this),la=E(this),V=E(this),ma=E(this),N=Array(V);for(g=0;g=I.a.length?a[e+0]=sa:a[c+d]=I.a[I.B++];break;case 54:var w=a[e+5];w||(a[e+5]=w=[]);w.push({K:E(this),A:E(this)});break;case 55:a[e+5].pop();break;case 59:(w=a[e+5])||(a[e+5]=w=[]);w.push({J:E(this),c:E(this),F:E(this),H:E(this)});break;case 56:var ba=E(this),ca=E(this),ta=E(this),J=E(this)^ba|0;for(e=ca;e>>13)>>>0;break;case 58:a[e+0]=a[c+E(this)];break;case 57:debugger; +break;default:throw Error("Unknown opcode: "+k+" at pc "+(a[e+0]-1));}}catch(y){e=0;for(k=this.s;k;){if((p=a[k+5])&&p.length>0){e=k;break}this.w(k,void 0,17);this.j=k;this.s=k=a[k+1]}if(!e)throw y;k=a[e+5].pop();p=a[e+7];k.A!==void 0?(a[p+k.A]=y,a[e+0]=k.K):(a[p+k.c]=k.H,a[p+k.F]=y,a[e+0]=k.J);this.j=e+a[e+6];this.s=e}if(!this.s)return this.f[0]}};var R=globalThis;typeof window!=="undefined"&&(R.window=window,R.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(R.module=module,R.exports=typeof exports!=="undefined"?exports:void 0);(new b(["window","TEST_OUTPUT","id",7,void 0,":"],null,"q",[4,1,47,3,52,1,4,0,0,5,2,3,2,3,0,0,0,4,1,0,0,5,2,0,42,6,2,1,5,0,5,3,0,42,7,6,1,5,42,5,7,0,9,3,4,5,0,3,4,0,45,3,4,2,47,3,71,1,4,1,0,1,0,45,3,0,3,4,0,45,3,4,2,47,3,92,0,5,2,0,0,0,1,0,45,3,0,3,4,0,45,3,4,1,3,2,0,0,3,5,0,11,4,2,3,3,2,1,11,3,4,2,45,3,0,2,4,0,45,2],R)).e("k",null,[],new B({m:0,l:8,v:0}),{},"k",void 0); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-171.js b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-171.js new file mode 100644 index 00000000..048ed9ba --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/encoded-171.js @@ -0,0 +1,16 @@ +function c(a,b,h,f){this.i=a;this.a=b;this.C=f;this.p=h;this.A=null;this.g=[];this.l=1;this.h=0}var B=new WeakMap;function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}function fa(a,b){this.g=a;this.v=b;this.u=!1;this.B=void 0}function C(a,b){this.e=b;this.j=[];this.prototype={};this.d=a} +c.prototype.c=function(a,b,h,f,l,m,k){f=m.e;l=this.g;for(var d=this.l,e=8+f.m,u=d+e;l.length>>13)&65535);return b}function ha(a,b,h){b=a.g[h+7]+b;h=a.A||(a.A=[]);return h[b]||(h[b]=new fa(a.g,b))}function ia(a){return a.u?a.B:a.g[a.v]}function Q(a,b){var h=a.A;if(h){var f=a.g;a=b+f[b+6];for(b=f[b+7];b=a.i.length)break;b[f+0]=m+1;m=a.i[m];try{var d=b[f+7];switch(m){case 0:var e=J(a);b[d+e]=D(a);break;case 1:e=J(a);b[d+e]=J(a);break;case 2:e=J(a);var u=D(a);if(!(u in a.p))throw new ReferenceError(`${u} is not defined`);b[d+e]=a.p[u];break;case 3:e=J(a);b[d+e]=ia(b[f+4].j[J(a)]);break;case 4:e=J(a);b[d+e]=b[f+3];break;case 5:e=J(a);b[d+e]=b[d+J(a)];break;case 6:a.p[D(a)]=b[d+J(a)];break;case 7:var q= +J(a);ja(b[f+4].j[q],b[d+J(a)]);break;case 8:e=J(a);var n=b[d+J(a)],p=b[d+J(a)];b[d+e]=n[p];break;case 9:n=b[d+J(a)];p=b[d+J(a)];var y=b[d+J(a)];Reflect.set(n,p,y);break;case 10:e=J(a);n=b[d+J(a)];p=b[d+J(a)];b[d+e]=delete n[p];break;case 11:e=J(a);var g=b[d+J(a)];b[d+e]=g+b[d+J(a)];break;case 12:e=J(a);g=b[d+J(a)];b[d+e]=g-b[d+J(a)];break;case 13:e=J(a);g=b[d+J(a)];b[d+e]=g*b[d+J(a)];break;case 14:e=J(a);g=b[d+J(a)];b[d+e]=g/b[d+J(a)];break;case 15:e=J(a);g=b[d+J(a)];b[d+e]=g%b[d+J(a)];break;case 60:e= +J(a);g=b[d+J(a)];b[d+e]=Math.pow(g,b[d+J(a)]);break;case 16:e=J(a);g=b[d+J(a)];b[d+e]=g&b[d+J(a)];break;case 17:e=J(a);g=b[d+J(a)];b[d+e]=g|b[d+J(a)];break;case 18:e=J(a);g=b[d+J(a)];b[d+e]=g^b[d+J(a)];break;case 19:e=J(a);g=b[d+J(a)];b[d+e]=g<>b[d+J(a)];break;case 21:e=J(a);g=b[d+J(a)];b[d+e]=g>>>b[d+J(a)];break;case 22:e=J(a);g=b[d+J(a)];b[d+e]=gb[d+J(a)];break;case 24:e=J(a);g=b[d+J(a)];b[d+ +e]=g<=b[d+J(a)];break;case 25:e=J(a);g=b[d+J(a)];b[d+e]=g>=b[d+J(a)];break;case 26:e=J(a);g=b[d+J(a)];b[d+e]=g===b[d+J(a)];break;case 27:e=J(a);g=b[d+J(a)];b[d+e]=g!==b[d+J(a)];break;case 28:e=J(a);g=b[d+J(a)];b[d+e]=g==b[d+J(a)];break;case 29:e=J(a);g=b[d+J(a)];b[d+e]=g!=b[d+J(a)];break;case 30:e=J(a);g=b[d+J(a)];b[d+e]=g in b[d+J(a)];break;case 31:e=J(a);n=b[d+J(a)];b[d+e]=n instanceof b[d+J(a)];break;case 32:e=J(a);b[d+e]=-b[d+J(a)];break;case 33:e=J(a);b[d+e]=+b[d+J(a)];break;case 34:e=J(a);b[d+ +e]=!b[d+J(a)];break;case 35:e=J(a);b[d+e]=~b[d+J(a)];break;case 36:e=J(a);b[d+e]=typeof b[d+J(a)];break;case 37:e=J(a);J(a);b[d+e]=void 0;break;case 38:e=J(a);var T=D(a);y=Object.prototype.hasOwnProperty.call(a.p,T)?a.p[T]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=J(a);break;case 40:var K=J(a),L=J(a);b[d+K]||(b[f+0]=L);break;case 41:K=J(a);L=J(a);b[d+K]&&(b[f+0]=L);break;case 42:e=J(a);var r=b[d+J(a)],t=J(a);if(t===65535)l=b[d+J(a)];else for(l=Array(t),k=0;k>1):0]=E;break;case 46:throw b[d+J(a)];case 47:e=J(a);var la=J(a),ma=J(a),na=J(a),W=J(a),oa=J(a),N=Array(W);for(k=0;k=H.k.length?b[f+0]=ua:b[d+e]=H.k[H.b++];break;case 54:var w=b[f+5];w||(b[f+5]=w=[]);w.push({F:J(a),r:J(a)});break;case 55:b[f+5].pop();break;case 59:(w=b[f+5])||(b[f+5]=w=[]);w.push({q:J(a),Q:J(a),x:J(a),z:J(a)});break;case 56:var ca=J(a),da=J(a),va=J(a),I=J(a)^ca|0;for(f=da;f>>13)>>>0;break;case 58:b[f+0]=b[d+J(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+ +" at pc "+(b[f+0]-1));}}catch(z){f=0;for(m=a.h;m;){var x=b[m+5];if(x&&x.length>0){f=m;break}Q(a,m);a.l=m;m=b[m+1];a.h=m}if(!f)throw z;m=b[f+5].pop();x=b[f+7];m.r!==void 0?(b[x+m.r]=z,b[f+0]=m.F):(b[x+m.Q]=m.z,b[x+m.x]=z,b[f+0]=m.q);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new c([4,1,47,3,52,1,4,0,0,5,2,3,2,3,0,0,0,4,1,0,0,5,2,0,42,6,2,1,5,0,5,3,0,42,7,6,1,5,42,5,7,0,9,3,4,5,0,3,4,0,45,3,4,2,47,3,71,1,4,1,0,1,0,45,3,0,3,4,0,45,3,4,2,47,3,92,0,5,2,0,0,0,1,0,45,3,0,3,4,0,45,3,4,1,3,2,0,0,3,5,0,11,4,2,3,3,2,1,11,3,4,2,45,3,0,2,4,0,45,2],54,S,["window","TEST_OUTPUT","id",7,void 0,":"]),void 0,new C("y",{o:0,m:8,D:0}),"y",null,72); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/source.js b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/source.js new file mode 100644 index 00000000..fbfe36eb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-runtime/nested-strings/source.js @@ -0,0 +1 @@ +function outer(prefix) { return function middle(n) { return function inner() { return prefix + ":" + n; }; }; } window.TEST_OUTPUT = outer("id")(7)(); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/encoded.js new file mode 100644 index 00000000..1d989482 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/encoded.js @@ -0,0 +1,17 @@ +var c={};function p(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var w=new WeakMap;function la(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ma(a){return a.C?a.F:a.g[a.B]}function na(a,g){a.C?a.F=g:a.g[a.B]=g}function C(a){this.U=a;this.o=[];this.prototype={}}function D(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function E(a){return a.j[a.g[a.h+0]++]} +function F(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,x=8+f.v,d=n+x;k.length>>13)&65535);return g}function T(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var x=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=x);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(x){case 0:var e=E(a);b[d+e]=G(a);break;case 1:e=E(a);b[d+e]=E(a);break;case 2:e=E(a);var M=G(a);if(!(M in a.i))throw new ReferenceError(`${M} is not defined`);b[d+e]=a.i[M];break; +case 3:e=E(a);b[d+e]=ma(b[f+4].o[E(a)]);break;case 4:e=E(a);b[d+e]=b[f+3];break;case 5:e=E(a);b[d+e]=b[d+E(a)];break;case 6:a.i[G(a)]=b[d+E(a)];break;case 7:var pa=E(a);na(b[f+4].o[pa],b[d+E(a)]);break;case 8:e=E(a);var q=b[d+E(a)],r=b[d+E(a)];b[d+e]=q[r];break;case 9:q=b[d+E(a)];r=b[d+E(a)];var z=b[d+E(a)];Reflect.set(q,r,z);break;case 10:e=E(a);q=b[d+E(a)];r=b[d+E(a)];b[d+e]=delete q[r];break;case 11:e=E(a);var h=b[d+E(a)];b[d+e]=h+b[d+E(a)];break;case 12:e=E(a);h=b[d+E(a)];b[d+e]=h-b[d+E(a)];break; +case 13:e=E(a);h=b[d+E(a)];b[d+e]=h*b[d+E(a)];break;case 14:e=E(a);h=b[d+E(a)];b[d+e]=h/b[d+E(a)];break;case 15:e=E(a);h=b[d+E(a)];b[d+e]=h%b[d+E(a)];break;case 60:e=E(a);h=b[d+E(a)];b[d+e]=Math.pow(h,b[d+E(a)]);break;case 16:e=E(a);h=b[d+E(a)];b[d+e]=h&b[d+E(a)];break;case 17:e=E(a);h=b[d+E(a)];b[d+e]=h|b[d+E(a)];break;case 18:e=E(a);h=b[d+E(a)];b[d+e]=h^b[d+E(a)];break;case 19:e=E(a);h=b[d+E(a)];b[d+e]=h<>b[d+E(a)];break;case 21:e=E(a);h=b[d+ +E(a)];b[d+e]=h>>>b[d+E(a)];break;case 22:e=E(a);h=b[d+E(a)];b[d+e]=hb[d+E(a)];break;case 24:e=E(a);h=b[d+E(a)];b[d+e]=h<=b[d+E(a)];break;case 25:e=E(a);h=b[d+E(a)];b[d+e]=h>=b[d+E(a)];break;case 26:e=E(a);h=b[d+E(a)];b[d+e]=h===b[d+E(a)];break;case 27:e=E(a);h=b[d+E(a)];b[d+e]=h!==b[d+E(a)];break;case 28:e=E(a);h=b[d+E(a)];b[d+e]=h==b[d+E(a)];break;case 29:e=E(a);h=b[d+E(a)];b[d+e]=h!=b[d+E(a)];break;case 30:e=E(a);h=b[d+E(a)];b[d+e]=h in b[d+E(a)]; +break;case 31:e=E(a);q=b[d+E(a)];b[d+e]=q instanceof b[d+E(a)];break;case 32:e=E(a);b[d+e]=-b[d+E(a)];break;case 33:e=E(a);b[d+e]=+b[d+E(a)];break;case 34:e=E(a);b[d+e]=!b[d+E(a)];break;case 35:e=E(a);b[d+e]=~b[d+E(a)];break;case 36:e=E(a);b[d+e]=typeof b[d+E(a)];break;case 37:e=E(a);E(a);b[d+e]=void 0;break;case 38:e=E(a);var Z=G(a);z=Object.prototype.hasOwnProperty.call(a.i,Z)?a.i[Z]:void 0;b[d+e]=typeof z;break;case 39:b[f+0]=E(a);break;case 40:var N=E(a),O=E(a);b[d+N]||(b[f+0]=O);break;case 41:N= +E(a);O=E(a);b[d+N]&&(b[f+0]=O);break;case 42:e=E(a);var t=b[d+E(a)],u=E(a);if(u===65535)l=b[d+E(a)];else for(l=Array(u),m=0;m>1):0]=H;break;case 46:throw b[d+E(a)];case 47:e=E(a);var ra=E(a),sa=E(a),ta=E(a),ca=E(a),ua=E(a),Q=Array(ca);for(m=0;m=K.J.length?b[f+0]=Aa:b[d+e]=K.J[K.M++];break;case 54:var y=b[f+5];y||(b[f+5]=y=[]);y.push({V:E(a),L:E(a)});break;case 55:b[f+5].pop();break;case 59:(y=b[f+5])||(b[f+5]=y=[]);y.push({T:E(a),S:E(a),Y:E(a),Z:E(a)});break;case 56:var ja=E(a),ka=E(a),Ba=E(a),L=E(a)^ja|0;for(f=ka;f>>13)>>>0;break;case 58:b[f+0]=b[d+E(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+x+" at pc "+(b[f+0]-1));}}catch(A){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}T(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw A;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=A,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=A,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var V=globalThis; +typeof window!=="undefined"&&(V.window=window,V.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(V.module=module,V.exports=typeof exports!=="undefined"?exports:void 0); +for(var Ca=U,W=p("OAAAAAUAAAC2AQAA+QEAAJOmW2kZAAAAFAAAAAQAAAAJAAAAIgAAAA8AAAAFAAAAIwAAABAAAAATAAAADAAAABIAAAAAAAAAAQAAADgAAACuAQAANAEAADcBAACOROQ2OAAAACQBAAB+AQAAiQEAAJFKZLsXAAAADQAAAAEAAAARAAAAEwAAAAsAAAABAAAADQAAADgAAAAHAgAAMgEAADsBAABGU58fGwAAABEAAAATAAAAFgAAAAEAAAAVAAAAaK4AAAQAAAAAAAAADQAAABEAAAAFAAAAEgAAAA4AAAAWAAAAFAAAABMAAAA4AAAAmAEAAAcCAAAKAgAAW4KSCw4AAAAJAAAAEgAAAAcAAAALAAAABwAAABUAAAAHAAAALQAAAAMAAAA4AAAATwAAABECAAATAgAAJSpjZAQAAAAMAAAAOwAAAM4AAAAEAAAABQAAAOAAAAA2AAAApAAAAAMAAAAAAAAABgAAAAAAAAAAAAAAFgAAAAcAAAAAAAAABgAAACgAAAAHAAAAcwAAAAIAAAAGAAAAAQAAAAAAAAAAAAAABwAAAAIAAAAAAAAALAAAAAgAAAAGAAAAAQAAAAcAAAAuAAAACAAAADgAAAB4AAAA+wAAAAMBAAAy1P78OAAAAKQBAACQAQAAlwEAAOTwsXo3AAAABAAAAA4AAAA4AAAAhQAAAAMBAAALAQAAAW7jnRUAAAANAAAADQAAABIAAAARAAAAFwAAAAYAAAAQAAAAJwAAAM4AAAALAAAAFQAAAAkAAAAQAAAAOAAAADEBAAAzAQAAOgEAAJB6Gvs3AAAAJwAAAL8AAAAQAAAADAAAAAUAAAAJAAAAOAAAAPsBAACMAQAAlQEAANYxwZwAAAAABgAAAAMAAAAAAAAAOAAAAK0AAABGAQAASgEAAHjgS0MkAAAABwAAAAcAAAAWAAAABQAAAAUAAAAHAAAAAQAAAAQAAADuAAAANwAAACcAAADOAAAAOAAAAIMBAAAAAgAABwIAAH9gdlc3AAAAAQAAAAQAAADwAAAAJwAAAM4AAAA4AAAAlgEAAJgBAACeAQAASALo4xsAAAAJAAAAFQAAABAAAAACAAAABgAAAAQAAAAAAAAAAAAAAAcAAAAFAAAAAAAAAAAAAAAIAAAABgAAAAAAAAAJAAAABgAAAAcAAAAIAAAAOgAAAAQAAAAuAAAABQAAAC0AAAAFAAAAOAAAAPoBAAAuAQAAMgEAALsZOn84AAAAPwEAAKABAACnAQAARSTbLC0AAAAFAAAAAAAAAAYAAAAHAAAAAAAAAC0AAAAGAAAAOAAAACUBAACHAQAAlAEAAD2BDAixlzKb1AxsOVf8o9fLFd91TZQUFPo8TrJArIFQwde97t0IGzxgs1Ta5B+KeGXjwRbmaP20XNEyU09JYPGCQJuPCwAAAAcAAAAOAAAAEwAAAA4AAAAKAAAAGAAAAAsAAAANAAAACQAAAAgAAAAKAAAAGQAAAAcAAAADAAAAEQAAACIAAAAQAAAAAwAAAAsAAAAYAAAABwAAABAAAAAUAAAAFQAAABMAAAAOAAAACwAAAAAAAAAHAAAAAAAAABoAAAACAAAADQAAABYAAAA4AAAAmgEAAJYBAACmAQAAmwTT6DgAAAACAgAAgQEAAIcBAACOvAzWCwAAAAkAAAANAAAAFwAAABMAAAAUAAAADQAAAA8AAAATAAAADAAAABYAAAAIAAAABAAAABUAAACcVoThlim5f5Gh8h3xJiy8FwAAABIAAAAXAAAACgAAABIAAAALAAAAFgAAAAAAAAAZAAAAEQAAABMAAAAVAAAADQAAAAkAAAAUAAAADAAAADgAAACfAQAABQIAABECAACgxdQGDgAAABAAAAAHAAAADgAAACQAAAASAAAAAwAAAAQAAAABAAAAFQAAABgAAAAGAAAAEgAAABUAAAAIAAAAFQAAAAIAAAA4AAAATQEAADEBAAA+AQAABVI5GjcAAAAkAAAADAAAAA8AAAA3AAAAMAAAABkAAAAXAAAAFgAAAAkAAAA4AAAA/QEAABQBAAAgAQAAT+6yozgAAAAyAQAAqgEAALMBAABNzH1kJAAAAAkAAAABAAAAOAAAAIwBAAAzAQAAQgEAALzaiBATAAAAAwAAAAkAAAAYAAAAEwAAABMAAAAPAAAAFQAAABoAAAAEAAAAFgAAABcAAAATAAAAFAAAABcAAAALAAAAFAAAAAIAAAAIAAAABwAAADcAAAA4AAAAiAEAABUBAAAhAQAAU8StqAEAAAAMAAAA/70AADgAAAACAgAAnAEAAKsBAAAuheqTNwAAADcAAAAsAAAA0hyTB120z6X+MwBEtZw+4vEEdYCpdageUxzavOWsFVtgJEn55tuCl2RMvDXrxPPTn0UvcpXNZBC3vJ6uvCTRTIrjDeuNf0aJlZR4J4MEt8WGvetjeQ0gAoGUUqCW+44+s2/A3JbU/Xq3tDUZn01pt63dplWZdNzzZ/MXkuZfSzB9I4TO4LS6bGEd8QrcjS2peAViR7aUkOW+L8uDSrsHIsAkOMBebXdex/2q/EJF4Jr7xB05SD9V19JTiXVQ58ITMH38sTvtM1A/lW/uLwWkjFWP1irWGwnJZfdFZ/9sfgVs3bCj7jXvQRW1I+Aa319+NUOTHDn3zroPUwBZBq099wAldZUHlagzBQ3m0SMAAAAVAAAAEAAAACQAAAADAAAADQAAACIAAAAEAAAABAAAABUAAAALAAAAAwAAABMAAAAPAAAAEgAAABQAAAAGAAAAGgAAAAMAAAARAAAACQAAACIAAAAJAAAAEQAAAPKwmgJOG9eg"),X= +new Uint32Array(W.length/4),Y=0;Y>>0;Ca(new D(X,[0,"Error","negative","message","window","finished",!0,void 0,1,"TEST_OUTPUT",4],V),new C({m:0,v:10,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/qualification.json new file mode 100644 index 00000000..89b0cd92 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/qualification.json @@ -0,0 +1,74 @@ +{ + "id": "exceptions--minifySelfModifyingEncodeBytecode--184", + "case": "exceptions", + "profile": "minifySelfModifyingEncodeBytecode", + "seed": 184, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": true, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": true, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "minify": true, + "selfModifying": true, + "encodeBytecode": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-self-modifying-encoded-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/encoded.js b/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/encoded.js new file mode 100644 index 00000000..6bd51a41 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */undefined, /* 4 */41095, /* 5 */0, /* 6 */17344, /* 7 */9967, /* 8 */40306, /* 9 */24412, /* 10 */8705, /* 11 */4730, /* 12 */11259, /* 13 */24763, /* 14 */6288, /* 15 */1708, /* 16 */29435, /* 17 */21550, /* 18 */31013, /* 19 */28812, /* 20 */33920, /* 21 */31153, /* 22 */5270, /* 23 */30280, /* 24 */41055, /* 25 */33926, /* 26 */49431, /* 27 */64115, /* 28 */59879, /* 29 */5709, /* 30 */40365, /* 31 */18433, /* 32 */19420, /* 33 */829, /* 34 */4360, /* 35 */40600, /* 36 */15491, /* 37 */22065, /* 38 */20332, /* 39 */15877, /* 40 */17824, /* 41 */35415, /* 42 */37169, /* 43 */15499, /* 44 */26182, /* 45 */19957, /* 46 */590, /* 47 */16464, /* 48 */8709, /* 49 */826, /* 50 */4128, /* 51 */29233, /* 52 */44448, /* 53 */7085, /* 54 */100, /* 55 */3, /* 56 */"k", /* 57 */2, /* 58 */5, /* 59 */4, /* 60 */"a", /* 61 */"Date", /* 62 */"Error", /* 63 */"String", /* 64 */"concat", /* 65 */"c", /* 66 */"b"]; +var BYTECODE = [4, 1, 47, 3, 43, 1, 174, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 42, 6, 2, 1, 5, 48, 5, 1, 6, 9, 3, 4, 5, 0, 3, 3, 0, 45, 3, 4, 24, 0, 25, 4, 0, 5, 26, 25, 0, 27, 5, 0, 5, 28, 27, 0, 29, 6, 0, 27, 30, 26, 29, 40, 30, 1008, 0, 31, 7, 0, 5, 28, 31, 26, 32, 26, 28, 40, 32, 86, 39, 1093, 0, 33, 8, 0, 11, 34, 28, 33, 5, 28, 34, 26, 35, 26, 28, 40, 35, 106, 39, 1271, 0, 36, 9, 0, 12, 37, 28, 36, 5, 28, 37, 26, 38, 26, 28, 40, 38, 126, 39, 1321, 0, 39, 10, 0, 11, 40, 28, 39, 5, 28, 40, 26, 41, 26, 28, 40, 41, 146, 39, 1807, 0, 42, 11, 0, 12, 43, 28, 42, 5, 28, 43, 26, 44, 26, 28, 40, 44, 166, 39, 1035, 0, 45, 12, 0, 11, 46, 28, 45, 5, 28, 46, 26, 47, 26, 28, 40, 47, 186, 39, 1536, 0, 48, 13, 0, 12, 49, 28, 48, 5, 28, 49, 26, 50, 26, 28, 40, 50, 206, 39, 1750, 0, 51, 14, 0, 12, 52, 28, 51, 5, 28, 52, 26, 53, 26, 28, 40, 53, 226, 39, 1008, 0, 54, 15, 0, 12, 55, 28, 54, 5, 28, 55, 26, 56, 26, 28, 40, 56, 246, 39, 1259, 0, 57, 16, 0, 11, 58, 28, 57, 5, 28, 58, 26, 59, 26, 28, 40, 59, 266, 39, 1287, 0, 60, 17, 0, 12, 61, 28, 60, 5, 28, 61, 26, 62, 26, 28, 40, 62, 286, 39, 1064, 0, 63, 18, 0, 11, 64, 28, 63, 5, 28, 64, 26, 65, 26, 28, 40, 65, 306, 39, 1180, 0, 66, 19, 0, 12, 67, 28, 66, 5, 28, 67, 26, 68, 26, 28, 40, 68, 326, 39, 1212, 0, 69, 20, 0, 11, 70, 28, 69, 5, 28, 70, 26, 71, 26, 28, 40, 71, 346, 39, 1587, 0, 72, 21, 0, 12, 73, 28, 72, 5, 28, 73, 26, 74, 26, 28, 40, 74, 366, 39, 1385, 0, 75, 22, 0, 11, 76, 28, 75, 5, 28, 76, 26, 77, 26, 28, 40, 77, 386, 39, 1700, 0, 78, 23, 0, 11, 79, 28, 78, 5, 28, 79, 26, 80, 26, 28, 40, 80, 406, 39, 1423, 0, 81, 24, 0, 12, 82, 28, 81, 5, 28, 82, 26, 83, 26, 28, 40, 83, 426, 39, 1516, 0, 84, 25, 0, 11, 85, 28, 84, 5, 28, 85, 26, 86, 26, 28, 40, 86, 446, 39, 1305, 0, 87, 26, 0, 12, 88, 28, 87, 5, 28, 88, 26, 89, 26, 28, 40, 89, 466, 39, 1432, 0, 90, 27, 0, 11, 91, 28, 90, 5, 28, 91, 26, 92, 26, 28, 40, 92, 486, 39, 1660, 0, 93, 28, 0, 12, 94, 28, 93, 5, 28, 94, 26, 95, 26, 28, 40, 95, 506, 39, 1196, 0, 96, 29, 0, 11, 97, 28, 96, 5, 28, 97, 26, 98, 26, 28, 40, 98, 526, 39, 1171, 0, 99, 30, 0, 11, 100, 28, 99, 5, 28, 100, 26, 101, 26, 28, 40, 101, 546, 39, 1783, 0, 102, 31, 0, 12, 103, 28, 102, 5, 28, 103, 26, 104, 26, 28, 40, 104, 566, 39, 1454, 0, 105, 32, 0, 12, 106, 28, 105, 5, 28, 106, 26, 107, 26, 28, 40, 107, 586, 39, 1554, 0, 108, 33, 0, 11, 109, 28, 108, 5, 28, 109, 26, 110, 26, 28, 40, 110, 606, 39, 1020, 0, 111, 34, 0, 12, 112, 28, 111, 5, 28, 112, 26, 113, 26, 28, 40, 113, 626, 39, 1107, 0, 114, 35, 0, 11, 115, 28, 114, 5, 28, 115, 26, 116, 26, 28, 40, 116, 646, 39, 1679, 0, 117, 36, 0, 11, 118, 28, 117, 5, 28, 118, 26, 119, 26, 28, 40, 119, 666, 39, 1250, 0, 120, 37, 0, 12, 121, 28, 120, 5, 28, 121, 26, 122, 26, 28, 40, 122, 686, 39, 1136, 0, 123, 38, 0, 11, 124, 28, 123, 5, 28, 124, 26, 125, 26, 28, 40, 125, 706, 39, 1608, 0, 126, 39, 0, 12, 127, 28, 126, 5, 28, 127, 26, 128, 26, 28, 40, 128, 726, 39, 1343, 0, 129, 40, 0, 12, 130, 28, 129, 5, 28, 130, 26, 131, 26, 28, 40, 131, 746, 39, 1337, 0, 132, 41, 0, 11, 133, 28, 132, 5, 28, 133, 26, 134, 26, 28, 40, 134, 766, 39, 1709, 0, 135, 42, 0, 12, 136, 28, 135, 5, 28, 136, 26, 137, 26, 28, 40, 137, 786, 39, 1574, 0, 138, 43, 0, 12, 139, 28, 138, 5, 28, 139, 26, 140, 26, 28, 40, 140, 806, 39, 1637, 0, 141, 44, 0, 11, 142, 28, 141, 5, 28, 142, 26, 143, 26, 28, 40, 143, 826, 39, 1493, 0, 144, 45, 0, 12, 145, 28, 144, 5, 28, 145, 26, 146, 26, 28, 40, 146, 846, 39, 1397, 0, 147, 46, 0, 11, 148, 28, 147, 5, 28, 148, 26, 149, 26, 28, 40, 149, 866, 39, 1484, 0, 150, 47, 0, 11, 151, 28, 150, 5, 28, 151, 26, 152, 26, 28, 40, 152, 886, 39, 1463, 0, 153, 48, 0, 12, 154, 28, 153, 5, 28, 154, 26, 155, 26, 28, 40, 155, 906, 39, 1729, 0, 156, 49, 0, 11, 157, 28, 156, 5, 28, 157, 26, 158, 26, 28, 40, 158, 926, 39, 1763, 0, 159, 50, 0, 11, 160, 28, 159, 5, 28, 160, 26, 161, 26, 28, 40, 161, 946, 39, 1365, 0, 162, 51, 0, 12, 163, 28, 162, 5, 28, 163, 26, 164, 26, 28, 40, 164, 966, 39, 1511, 0, 165, 52, 0, 11, 166, 28, 165, 5, 28, 166, 26, 167, 26, 28, 40, 167, 986, 39, 1165, 0, 168, 53, 0, 12, 169, 28, 168, 5, 28, 169, 26, 170, 26, 28, 40, 170, 1006, 39, 1241, 39, 59, 5, 7, 15, 1, 171, 53419, 11, 26, 26, 171, 39, 59, 32, 172, 3, 5, 3, 172, 1, 171, 29666, 11, 26, 26, 171, 39, 59, 0, 172, 54, 0, 23, 173, 3, 172, 41, 173, 1055, 1, 171, 20731, 12, 26, 26, 171, 39, 59, 1, 171, 2197, 11, 26, 26, 171, 39, 59, 0, 172, 55, 0, 26, 173, 3, 172, 41, 173, 1084, 1, 171, 15835, 11, 26, 26, 171, 39, 59, 1, 171, 6097, 12, 26, 26, 171, 39, 59, 5, 3, 17, 58, 10, 1, 171, 7167, 12, 26, 26, 171, 39, 59, 0, 172, 2, 0, 26, 173, 3, 172, 41, 173, 1127, 1, 171, 7116, 11, 26, 26, 171, 39, 59, 1, 171, 17354, 11, 26, 26, 171, 39, 59, 0, 172, 56, 0, 30, 173, 172, 4, 41, 173, 1156, 1, 171, 6574, 11, 26, 26, 171, 39, 59, 1, 171, 7343, 12, 26, 26, 171, 39, 59, 0, 172, 3, 0, 45, 172, 1, 171, 15737, 11, 26, 26, 171, 39, 59, 11, 172, 3, 7, 5, 3, 172, 1, 171, 19329, 12, 26, 26, 171, 39, 59, 0, 172, 57, 0, 5, 3, 172, 1, 171, 14319, 11, 26, 26, 171, 39, 59, 0, 172, 2, 0, 26, 173, 7, 172, 41, 173, 1232, 1, 171, 28812, 11, 26, 26, 171, 39, 59, 1, 171, 21741, 11, 26, 26, 171, 39, 59, 1, 171, 12258, 12, 26, 26, 171, 39, 59, 1, 171, 35360, 12, 26, 26, 171, 39, 59, 42, 172, 23, 1, 3, 44, 173, 22, 1, 172, 46, 173, 0, 172, 58, 0, 5, 3, 172, 1, 171, 31539, 12, 26, 26, 171, 39, 59, 43, 172, 19, 20, 1, 21, 5, 18, 172, 1, 171, 25135, 12, 26, 26, 171, 39, 59, 11, 172, 3, 14, 5, 3, 172, 1, 171, 15044, 12, 26, 26, 171, 39, 59, 0, 172, 59, 0, 5, 3, 172, 1, 171, 24412, 11, 26, 26, 171, 39, 59, 11, 172, 3, 12, 45, 172, 0, 172, 2, 0, 0, 173, 57, 0, 48, 21, 2, 172, 173, 1, 171, 9815, 12, 26, 26, 171, 39, 59, 5, 172, 3, 0, 172, 2, 0, 12, 16, 3, 172, 1, 171, 10844, 12, 26, 26, 171, 39, 59, 5, 3, 16, 1, 171, 8647, 11, 26, 26, 171, 39, 59, 47, 172, 1841, 0, 3, 2, 0, 1, 3, 1, 0, 5, 8, 172, 54, 1493, 9, 1, 171, 8345, 11, 26, 26, 171, 39, 59, 1, 171, 40518, 12, 26, 26, 171, 39, 59, 0, 172, 60, 0, 0, 173, 2, 0, 49, 13, 1, 172, 173, 1, 171, 50310, 11, 26, 26, 171, 39, 59, 1, 171, 18217, 11, 26, 26, 171, 39, 59, 2, 172, 61, 0, 44, 173, 172, 1, 3, 5, 18, 173, 1, 171, 23152, 12, 26, 26, 171, 39, 59, 1, 171, 23807, 11, 26, 26, 171, 39, 59, 1, 26, 38691, 36, 172, 9, 5, 3, 172, 1, 171, 13651, 11, 26, 26, 171, 39, 59, 1, 26, 2800, 46, 11, 0, 172, 60, 0, 10, 173, 4, 172, 52, 5, 4, 1, 171, 18882, 11, 26, 26, 171, 39, 59, 4, 2, 5, 3, 0, 0, 12, 2, 0, 1, 171, 11333, 12, 26, 26, 171, 39, 59, 5, 3, 18, 2, 22, 62, 0, 2, 23, 63, 0, 1, 171, 4300, 12, 26, 26, 171, 39, 59, 1, 26, 28008, 55, 1, 171, 24334, 11, 26, 26, 171, 39, 59, 41, 0, 1599, 1, 171, 38877, 12, 26, 26, 171, 39, 59, 1, 171, 33018, 12, 26, 26, 171, 39, 59, 0, 172, 55, 0, 22, 173, 7, 172, 41, 173, 1628, 1, 171, 1733, 11, 26, 26, 171, 39, 59, 1, 171, 45041, 12, 26, 26, 171, 39, 59, 1, 26, 12509, 5, 172, 3, 0, 172, 2, 0, 11, 17, 3, 172, 1, 171, 2542, 12, 26, 26, 171, 39, 59, 1, 26, 64294, 0, 172, 5, 0, 5, 7, 172, 1, 171, 831, 12, 26, 26, 171, 39, 59, 48, 19, 1, 3, 0, 172, 64, 0, 8, 20, 19, 172, 1, 171, 2119, 12, 26, 26, 171, 39, 59, 1, 171, 22044, 12, 26, 26, 171, 39, 59, 5, 4, 13, 0, 172, 65, 0, 9, 4, 172, 0, 1, 171, 49493, 12, 26, 26, 171, 39, 59, 59, 1637, 10, 11, 1511, 42, 172, 8, 0, 5, 3, 172, 1, 171, 10747, 12, 26, 26, 171, 39, 59, 55, 1, 10, 1574, 1, 171, 3823, 12, 26, 26, 171, 39, 59, 5, 172, 7, 0, 172, 2, 0, 11, 15, 7, 172, 1, 171, 17861, 12, 26, 26, 171, 39, 59, 0, 172, 66, 0, 47, 173, 1827, 0, 3, 0, 0, 50, 13, 172, 173, 1, 171, 14688, 11, 26, 26, 171, 39, 59, 53, 172, 5, 1660, 5, 6, 172, 8, 14, 4, 6, 1, 171, 15044, 11, 26, 26, 171, 39, 59, 4, 1, 0, 2, 57, 0, 45, 2, 0, 2, 3, 0, 45, 2, 3, 0, 0, 3, 1, 1, 11, 2, 0, 1, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/generation.json b/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/generation.json new file mode 100644 index 00000000..38ca3b49 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/generation.json @@ -0,0 +1,12 @@ +{ + "note": "encoded.js is the VM encoder's output for source.js with controlFlowFlattening, and plain.encoded.js with no options, each with the leading disassembly comment block removed (the committed corpus format). Both encodes replaced Math.random with the seeded PRNG below (mulberry32: state += 0x6d2b79f5, then the standard mulberry32 output); the plain encode draws no randomness. Regenerating with the same encoder revision, options and seed reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5" + }, + "files": { + "encoded.js": { "options": { "controlFlowFlattening": true }, "seed": 1 }, + "plain.encoded.js": { "options": {}, "seed": 1 } + }, + "claim": "source dead code after a return, with closures only the dead code creates, under control flow flattening: the CFF pass removes only the fake blocks it injected, so the normalized stream holds the plain compile's instructions apart from jumps, and the program decodes with the source's behaviour" +} diff --git a/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/plain.encoded.js b/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/plain.encoded.js new file mode 100644 index 00000000..82386bf8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/plain.encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */undefined, /* 4 */"a", /* 5 */"b", /* 6 */"c", /* 7 */0, /* 8 */3, /* 9 */100, /* 10 */2, /* 11 */4, /* 12 */5, /* 13 */"k", /* 14 */"Date", /* 15 */"concat", /* 16 */"Error", /* 17 */"String"]; +var BYTECODE = [4, 1, 47, 3, 43, 1, 18, 0, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 1, 0, 0, 5, 2, 0, 42, 6, 2, 1, 5, 48, 5, 1, 6, 9, 3, 4, 5, 0, 3, 3, 0, 45, 3, 4, 2, 5, 3, 0, 0, 12, 2, 0, 11, 13, 3, 12, 45, 13, 0, 12, 4, 0, 0, 13, 2, 0, 49, 14, 1, 12, 13, 0, 12, 5, 0, 47, 13, 421, 0, 3, 0, 0, 50, 14, 12, 13, 5, 4, 14, 0, 12, 6, 0, 9, 4, 12, 0, 0, 12, 4, 0, 10, 13, 4, 12, 52, 5, 4, 53, 12, 5, 128, 5, 6, 12, 8, 12, 4, 6, 11, 13, 3, 12, 5, 3, 13, 39, 108, 0, 12, 7, 0, 5, 7, 12, 0, 12, 8, 0, 22, 13, 7, 12, 40, 13, 182, 0, 12, 2, 0, 26, 13, 7, 12, 40, 13, 159, 39, 166, 11, 12, 3, 7, 5, 3, 12, 5, 12, 7, 0, 12, 2, 0, 11, 13, 7, 12, 5, 7, 13, 39, 135, 0, 12, 9, 0, 23, 13, 3, 12, 40, 13, 209, 5, 12, 3, 0, 12, 2, 0, 12, 13, 3, 12, 5, 3, 13, 39, 182, 0, 12, 2, 0, 26, 13, 3, 12, 40, 13, 222, 39, 237, 0, 12, 8, 0, 26, 13, 3, 12, 40, 13, 235, 39, 246, 39, 253, 0, 12, 10, 0, 5, 3, 12, 39, 260, 0, 12, 11, 0, 5, 3, 12, 0, 12, 12, 0, 5, 3, 12, 47, 12, 435, 0, 3, 2, 0, 1, 3, 1, 0, 5, 8, 12, 54, 316, 9, 59, 295, 10, 11, 311, 42, 12, 8, 0, 5, 3, 12, 55, 1, 10, 313, 39, 295, 5, 12, 3, 0, 12, 2, 0, 11, 13, 3, 12, 5, 3, 13, 58, 10, 46, 11, 55, 39, 322, 36, 12, 9, 5, 3, 12, 40, 0, 327, 39, 333, 32, 12, 3, 5, 3, 12, 0, 12, 13, 0, 30, 13, 12, 4, 40, 13, 358, 2, 12, 14, 0, 44, 13, 12, 1, 3, 5, 12, 13, 39, 392, 48, 13, 1, 3, 0, 14, 15, 0, 8, 15, 13, 14, 0, 14, 2, 0, 0, 16, 10, 0, 48, 17, 2, 14, 16, 43, 14, 13, 15, 1, 17, 5, 12, 14, 5, 3, 12, 2, 12, 16, 0, 2, 13, 17, 0, 42, 14, 13, 1, 3, 44, 13, 12, 1, 14, 46, 13, 0, 12, 3, 0, 45, 12, 4, 1, 0, 2, 10, 0, 45, 2, 0, 2, 3, 0, 45, 2, 3, 0, 0, 3, 1, 1, 11, 2, 0, 1, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/source.js b/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/source.js new file mode 100644 index 00000000..99e41596 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/normalize-cff-wordcode/dead-code/source.js @@ -0,0 +1,20 @@ +// Source dead code under control flow flattening: everything after the return is unreachable, +// with a getter, an arrow closure, enumeration, loops, a switch, nested try/finally/catch, a +// labeled break and a throw. CFF adds its own fake blocks; only those are its to remove. +function deadAll(x) { + let total = x + return total + 1 + const o = { a: 1, get b() { return 2 } } + o.c = x + delete o.a + for (const k in o) total += o[k] + for (let i = 0; i < 3; i++) { if (i === 1) continue; total += i } + while (total > 100) total-- + switch (total) { case 1: total = 2; break; case 3: total = 4; default: total = 5 } + const f = () => total + x + try { try { total = f() } finally { total++ } } catch (e) { total = typeof e } + label: { if (x) break label; total = -total } + total = 'k' in o ? new Date(total) : [total, ...[1, 2]] + throw new Error(String(total)) +} +window.TEST_OUTPUT = [deadAll(1)] diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/alias.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/alias.js new file mode 100644 index 00000000..3ae5ad98 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/alias.js @@ -0,0 +1,1135 @@ +var CONSTANTS = [/* 0 */6, /* 1 */7, /* 2 */3, /* 3 */5, /* 4 */undefined, /* 5 */"window", /* 6 */"TEST_OUTPUT"]; +var BYTECODE = [56, 5, 395, 405, 2705093750, 34, 2, 2, 55, 23, 18, 20, 10, 55, 32, 56, 20, 63, 99, 1853404710, 56, 254, 251, 260, 129718667, 13, 1, 6, 21, 56, 386, 258, 264, 2002891106, 34, 10, 24, 19, 15, 0, 11, 56, 255, 285, 290, 2165858660, 56, 276, 365, 378, 448859617, 14, 1, 2, 5, 39, 56, 61, 321, 323, 3959220100, 55, 26, 212879959, 2867003033, 1226664868, 3880888230, 2240428674, 600084133, 3254453673, 1614250587, 4268494508, 2628227698, 987441140, 3642008137, 2001362661, 360837720, 3015525839, 1374871108, 4028647940, 2388342730, 748149341, 3402133201, 1762064975, 121443826, 2775682676, 1135477486, 3789854237, 2149519905, 508722725, 3163546168, 1522751080, 4177477157, 2536819228, 895895150, 3550084045, 1909812855, 269408783, 2923472410, 56, 377, 356, 357, 11546470, 27, 18, 2, 10, 5, 5, 0, 4, 16, 8, 8, 9, 10, 56, 262, 362, 364, 2788544926, 56, 291, 346, 353, 2121685088, 56, 285, 259, 266, 2016657935, 23, 21, 13, 18, 17, 17, 3, 16, 16, 15, 5, 8, 24, 23, 14, 23, 4, 24, 56, 251, 337, 344, 2112255272, 36, 10, 11, 55, 3, 56, 378, 115, 125, 3173358102, 21, 12, 15, 22, 34, 15, 11, 22, 9, 14, 12, 56, 326, 209, 218, 1295014798, 55, 8, 8, 19, 14, 56, 249, 386, 390, 592580987, 26, 0, 5, 4, 8, 14, 3, 18, 8, 6, 13, 16, 26, 16, 8, 23, 14, 17, 14, 0, 12, 11, 17, 6, 36, 0, 24, 32, 5, 19, 56, 234, 300, 311, 1392862651, 56, 233, 107, 120, 302753861, 17, 4, 16, 16, 18, 21, 19, 24, 21, 24, 13, 12, 20, 2, 13, 10, 14, 15, 6, 4, 22, 18, 11, 24, 35, 8, 9, 20, 6, 18, 16, 26, 20, 10, 9, 21, 15, 13, 14, 28, 19, 23, 20, 8, 15, 0, 24, 21, 23, 6, 5, 9, 21, 19, 23, 22, 21, 11, 8, 13, 17, 22, 20, 12, 56, 272, 309, 315, 2383595254, 56, 266, 303, 307, 2640596461, 56, 272, 280, 293, 4045564683, 13, 2, 12, 19, 36, 13, 8, 22, 12, 5, 12, 2318413973, 678074487, 56, 256, 339, 351, 761258985, 25, 6, 10, 0, 21, 6, 1, 23, 17, 14, 18, 17, 56, 255, 274, 290, 3245231503, 56, 335, 296, 301, 3672113915, 21, 20, 10, 8, 12, 24, 12, 10, 4, 3, 55, 5, 21, 20, 21, 12, 2, 23, 27, 8, 15, 7, 17, 17, 19, 14, 15, 23, 19, 18, 19, 16, 15, 24, 1, 11, 34454, 12, 17, 20, 13, 18, 5, 5, 21, 1064441229, 3719137983, 2078359222, 437947269, 3092518812, 1452014214, 4106710713, 2466043391, 825111177, 3479260823]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ALIAS_LOAD_CONST_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 62: + { + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 63: + { + // ALIAS_LOAD_GLOBAL_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 64: + { + // ALIAS_MUL_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a * regs[base + _operands[2]]; + break; + } + case 65: + { + // ALIAS_ADD_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + break; + } + case 66: + { + // ALIAS_MOD_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a % regs[base + _operands[2]]; + break; + } + case 67: + { + // ALIAS_SET_PROP_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + _operands[0]]; + var key = regs[base + _operands[1]]; + var val = regs[base + _operands[2]]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 68: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/alias.src.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/alias.src.js new file mode 100644 index 00000000..e063dee6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/alias.src.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/anti.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/anti.js new file mode 100644 index 00000000..c1a6b9e6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/anti.js @@ -0,0 +1,1087 @@ +var CONSTANTS = [/* 0 */5, /* 1 */undefined, /* 2 */"TEST_OUTPUT", /* 3 */6, /* 4 */7, /* 5 */3, /* 6 */"GwUDm", /* 7 */"window", /* 8 */62072, /* 9 */44867, /* 10 */"Dnqew1cI", /* 11 */14351, /* 12 */0]; +var BYTECODE = [56, 5, 405, 425, 1008297014, 20, 6, 10, 11, 4, 3, 32, 5, 19, 56, 447, 177, 188, 1392862651, 56, 447, 356, 369, 302753861, 26, 56, 30, 399, 405, 2468234951, 56, 387, 375, 381, 3428083981, 59, 56, 41, 233, 268, 2236695576, 8, 13, 12, 16, 56, 355, 268, 282, 2886238422, 11, 19, 18, 19, 15, 15, 22, 18, 56, 363, 286, 300, 2425099559, 34, 11, 22, 8, 3, 7, 17, 56, 287, 448, 454, 238713766, 17, 56, 81, 111, 139, 853011545, 1, 3, 23303, 55, 56, 144, 369, 385, 3116741491, 8, 6, 13, 16, 26, 16, 8, 23, 14, 17, 14, 0, 12, 11, 17, 6, 36, 0, 24, 45, 7, 3507088826, 1866835269, 226374337, 2880474946, 1240400880, 3894642547, 2253943542, 613588302, 3268224474, 1627497812, 4282244904, 2641709872, 1000916775, 3655752486, 2015087901, 374289895, 3028460907, 1388113659, 4042421103, 2401834772, 761627419, 3415883018, 1775277361, 134944049, 2789735695, 1148742407, 3803633412, 2163030797, 17, 17, 19, 14, 15, 23, 19, 18, 19, 16, 15, 24, 1, 11, 34454, 12, 17, 20, 13, 34, 5, 21, 14, 15, 6, 4, 22, 18, 11, 24, 35, 8, 9, 20, 6, 18, 16, 26, 20, 10, 9, 21, 15, 13, 14, 25, 4, 24, 15, 34, 2, 2, 55, 23, 18, 20, 10, 56, 393, 297, 300, 2297122391, 24, 3, 12, 9, 15, 9, 9, 23, 35, 8, 6, 16, 7, 16, 16, 24, 11, 2, 16, 11, 9, 21, 7, 21, 15, 17, 19, 56, 223, 273, 281, 3063291610, 596222383, 3250993578, 1609912035, 4264090220, 2623686651, 983550590, 3637731055, 1997449617, 356786582, 3011092097, 1370616507, 4025321354, 2384855992, 744001162, 3398906497, 1758173572, 117465466, 2771581582, 1131239325, 3785477009, 2145238693, 504711864, 3158888889, 1518762405, 4173062822, 2532775782, 891990994, 3546550143, 1905885941, 265419094, 2920051152, 1279400280, 3933251260, 2292919284, 652730176, 15, 0, 7, 10, 56, 300, 448, 449, 1386881141, 26, 11, 7, 5, 8, 8, 15, 15, 56, 275, 139, 153, 1334027174, 16, 0, 4, 10, 13, 3, 2, 6, 4, 2, 56, 358, 439, 444, 763861455, 42, 56, 425, 443, 446, 3534576671, 22, 16, 9, 5, 35, 12, 2, 56, 219, 154, 163, 332613559, 56, 434, 344, 348, 4272053457, 56, 328, 222, 229, 71093068, 17, 12, 3, 5, 36, 20, 0, 27, 1, 18, 17, 32, 7, 21, 34, 12, 23, 21, 20, 19, 4, 25, 6, 10, 0, 21, 6, 1, 23, 17, 14, 18, 17, 56, 382, 143, 159, 3245231503, 56, 439, 171, 176, 3672113915, 23, 10, 8, 22, 56, 174, 142, 143, 11546470, 27, 18, 2, 10, 5, 5, 0, 4, 16, 8, 8, 9, 10, 10, 827607608, 3482011171, 1841814051, 201093723, 2855728876, 1214866038, 3663073130, 2022413284, 381641896, 3035819181, 1395416486, 4049712573, 2409219238, 768892754, 3423259601, 1783068494, 142335216, 2796956999, 1156101580, 3810858309, 2170391757, 529531716, 3184367423, 1543318732, 4197498201, 2557093328, 34, 2, 5, 19, 2, 2, 16, 27, 11, 12, 5, 56, 281, 277, 288, 2585180553, 25, 13, 18, 19, 21, 3, 16, 10, 19, 5, 8, 12, 27, 14, 23, 11, 4, 24, 55, 25]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MUL_8_0_5_1_3_6_4_7_2 (order: [8,0,5,1,3,6,4,7,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[0]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + break; + } + case 62: + { + // ANTI_ADD_6_5_3_7_0_2_4_1 (order: [6,5,3,7,0,2,4,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[3]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // JUMP_IF_FALSE + var src = _operands[3]; + var target = _operands[4]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[5]; + var name = this._constant(_operands[6], _operands[7]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + case 63: + { + // ANTI_MOD_0_5_13_1_6_12_2_9_11_7_8_10_14_3_4 (order: [0,5,13,1,6,12,2,9,11,7,8,10,14,3,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[13], _unsortedOperands[14], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[12]]; + // MOD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a % regs[base + _operands[2]]; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // SHR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >> regs[base + _operands[8]]; + // EQ + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a === regs[base + _operands[11]]; + // ADD + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a + regs[base + _operands[14]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/anti.src.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/anti.src.js new file mode 100644 index 00000000..e063dee6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/anti.src.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/arrays.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/arrays.js new file mode 100644 index 00000000..09a79a04 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/arrays.js @@ -0,0 +1,1360 @@ +var CONSTANTS = [/* 0 */1, /* 1 */2, /* 2 */3, /* 3 */4, /* 4 */5, /* 5 */6, /* 6 */7, /* 7 */8, /* 8 */9, /* 9 */10, /* 10 */11, /* 11 */12, /* 12 */13, /* 13 */14, /* 14 */15, /* 15 */16, /* 16 */17, /* 17 */18, /* 18 */19, /* 19 */20, /* 20 */21, /* 21 */22, /* 22 */23, /* 23 */24, /* 24 */"window", /* 25 */"TEST_OUTPUT", /* 26 */undefined]; +var BYTECODE = [61, 62, 63, 64, 65, 48, 12, 4, 8, 9, 10, 11, 66, 76, 8, 4, 0, 9, 5, 0, 10, 6, 0, 11, 7, 0, 12, 4, 8, 9, 10, 11, 67, 76, 8, 8, 0, 9, 9, 0, 10, 10, 0, 11, 11, 0, 12, 4, 8, 9, 10, 11, 56, 57, 215, 216, 1338898633, 23, 76, 8, 12, 0, 9, 13, 0, 10, 14, 0, 11, 15, 0, 12, 4, 8, 9, 10, 11, 69, 76, 8, 16, 0, 9, 17, 0, 10, 18, 0, 11, 19, 0, 12, 4, 8, 9, 10, 11, 84, 12, 6, 76, 8, 20, 0, 9, 21, 0, 10, 22, 0, 11, 23, 0, 12, 4, 8, 9, 10, 11, 84, 12, 7, 70, 75, 9, 25, 0, 10, 6, 2, 3, 4, 5, 6, 7, 71, 85, 0, 8, 26, 72, 25, 1, 16, 21, 19, 22, 11, 7, 56, 479, 154, 160, 75844354, 32, 20, 0, 11, 11, 12, 0, 56, 464, 144, 151, 1180176892, 55, 3, 15, 1, 24, 5, 23, 10, 14, 5, 20, 6, 10, 11, 4, 3, 32, 5, 19, 56, 200, 479, 490, 1392862651, 56, 200, 413, 426, 302753861, 17, 4, 16, 16, 18, 21, 19, 24, 21, 24, 13, 12, 5, 13, 10, 55, 1, 16, 38512, 18, 3993109190, 23, 17, 12, 15, 22, 5, 15, 11, 22, 9, 14, 12, 56, 143, 262, 271, 1295014798, 55, 8, 8, 19, 14, 56, 277, 487, 491, 592580987, 26, 0, 5, 4, 8, 14, 3, 18, 8, 6, 13, 16, 26, 16, 8, 23, 14, 17, 14, 0, 12, 11, 17, 6, 26, 0, 24, 9, 22, 14, 7, 11, 23, 18, 15, 15, 18, 24, 12, 3, 16, 6, 9, 17, 16, 14, 5, 5, 11, 10, 15, 6, 56, 318, 293, 306, 1885507148, 12, 9, 23, 16, 21, 16, 21, 22, 13, 9, 18, 17, 17, 14, 11, 21, 56, 275, 467, 477, 408655598, 55, 23, 18, 20, 10, 56, 490, 487, 490, 2297122391, 22, 17, 8, 11, 56, 350, 288, 303, 1071404078, 14, 0, 9, 16, 24, 8, 7, 11, 56, 284, 309, 322, 989058234, 25, 18, 19, 23, 14, 17, 11, 8, 21, 13, 22, 20, 34, 6, 1, 25, 0, 13, 1, 34, 5, 17, 27, 15, 1, 1, 56, 145, 389, 394, 763861455, 27, 17, 20, 19, 56, 271, 388, 393, 1143460340, 35, 1, 23, 17, 14, 18, 17, 56, 314, 334, 350, 3245231503, 3, 56, 480, 158, 166, 3461520427, 11, 22, 5, 5, 56, 346, 331, 341, 3999881398, 5, 10, 14, 56, 475, 333, 346, 2606459491, 11, 9, 10, 4, 34, 2, 5, 19, 2, 2, 16, 27, 11, 12, 5, 56, 294, 287, 298, 2585180553, 25, 13, 18, 19, 21, 3, 16, 10, 19, 5, 8, 12, 4, 23, 56, 307, 376, 377, 1742852474, 56, 475, 285, 293, 1534129441, 15, 5, 12, 8, 8, 12, 10, 15, 56, 334, 321, 322, 3745507913, 35, 18, 12, 56, 299, 468, 483, 1354207185, 4, 13]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 13; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 62: + { + // LOAD_CONST_8_0_0 (specialized) + var dst = 8; + regs[base + dst] = this._constant(0, 0); + break; + } + case 63: + { + // LOAD_CONST_9_1_0 (specialized) + var dst = 9; + regs[base + dst] = this._constant(1, 0); + break; + } + case 64: + { + // LOAD_CONST_10_2_0 (specialized) + var dst = 10; + regs[base + dst] = this._constant(2, 0); + break; + } + case 65: + { + // LOAD_CONST_11_3_0 (specialized) + var dst = 11; + regs[base + dst] = this._constant(3, 0); + break; + } + case 66: + { + // MOVE_2_12 (specialized) + var dst = 2; + regs[base + dst] = regs[base + 12]; + break; + } + case 67: + { + // MOVE_3_12 (specialized) + var dst = 3; + regs[base + dst] = regs[base + 12]; + break; + } + case 68: + { + // MOVE_4_12 (specialized) + var dst = 4; + regs[base + dst] = regs[base + 12]; + break; + } + case 69: + { + // MOVE_5_12 (specialized) + var dst = 5; + regs[base + dst] = regs[base + 12]; + break; + } + case 70: + { + // LOAD_GLOBAL_8_24_0 (specialized) + var dst = 8; + var globalName = this._constant(24, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 71: + { + // SET_PROP_8_9_10 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 8]; + var key = regs[base + 9]; + var val = regs[base + 10]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 72: + { + // RETURN_8 (specialized) + var retVal = regs[base + 8]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 73: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 74: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 75: + { + // LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 76: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 77: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 78: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 79: + { + // LOAD_CONST,LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 80: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,BUILD_ARRAY,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 81: + { + // LOAD_CONST,LOAD_CONST,BUILD_ARRAY,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 82: + { + // LOAD_CONST,BUILD_ARRAY,MOVE (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 83: + { + // BUILD_ARRAY,MOVE (macro) + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + // MOVE + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case 84: + { + // ALIAS_MOVE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + break; + } + case 85: + { + // ALIAS_LOAD_CONST_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/arrays.src.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/arrays.src.js new file mode 100644 index 00000000..4fc16690 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/arrays.src.js @@ -0,0 +1 @@ +const a = [1, 2, 3, 4]; const b = [5, 6, 7, 8]; const c = [9, 10, 11, 12]; const d = [13, 14, 15, 16]; const e = [17, 18, 19, 20]; const f = [21, 22, 23, 24]; window.TEST_OUTPUT = [a, b, c, d, e, f]; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/finally.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/finally.js new file mode 100644 index 00000000..f35191d6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/finally.js @@ -0,0 +1,1325 @@ +var CONSTANTS = [/* 0 */"nqew1", /* 1 */"vHRH3", /* 2 */1, /* 3 */undefined, /* 4 */20961, /* 5 */48, /* 6 */"YYqow1TW", /* 7 */0, /* 8 */"window", /* 9 */"TEST_OUTPUT", /* 10 */4, /* 11 */"Eux8", /* 12 */"D6WUL"]; +var BYTECODE = [56, 5, 223, 226, 3418723506, 55, 4, 3, 56, 13, 384, 385, 2828368398, 44, 56, 19, 289, 341, 3740833379, 55, 21, 19, 4, 23, 56, 441, 403, 406, 1535063842, 24, 19, 24, 13, 8, 13, 12, 16, 56, 387, 415, 429, 2886238422, 11, 19, 18, 19, 15, 15, 22, 18, 56, 391, 437, 451, 2425099559, 34, 11, 22, 8, 3, 7, 17, 56, 437, 370, 376, 238713766, 36, 4, 4, 51, 82, 6, 0, 0, 5, 1, 0, 56, 83, 459, 475, 3948619316, 56, 257, 279, 288, 538041785, 14, 24, 6, 13, 16, 22, 16, 8, 36, 9, 11, 61, 6, 6, 8, 5, 6, 6, 4, 5, 74, 55, 63, 55, 75, 63, 76, 61, 6, 6, 7, 6, 6, 6, 8, 6, 83, 8, 0, 2, 56, 133, 475, 485, 2038231736, 15, 17, 6, 22, 56, 373, 447, 451, 661359168, 29, 77, 78, 56, 150, 341, 342, 2731996663, 28, 56, 156, 458, 459, 1628481032, 55, 83, 7, 0, 3, 81, 56, 423, 430, 443, 2910064460, 27, 20, 11, 2, 35, 7, 9, 36, 8, 17, 21, 19, 0, 22, 56, 393, 260, 275, 1000988437, 18, 3, 13, 0, 18, 18, 16, 9, 56, 386, 417, 425, 3416346098, 56, 385, 417, 430, 2583648503, 35, 23, 7, 4, 22, 13, 20, 14, 0, 17, 12, 3, 5, 36, 20, 0, 1, 23, 5004, 1778270680, 137677260, 2791912128, 35, 9, 17, 16, 14, 5, 5, 11, 10, 15, 6, 56, 371, 393, 406, 1885507148, 12, 9, 23, 16, 21, 16, 21, 22, 13, 9, 18, 17, 17, 14, 11, 21, 56, 344, 389, 399, 408655598, 55, 23, 18, 20, 10, 56, 410, 448, 451, 2297122391, 15, 0, 7, 10, 56, 451, 369, 370, 1386881141, 26, 11, 7, 5, 1, 24, 23293, 2100558214, 459756249, 3113930532, 1473561574, 4127868776, 2487341804, 847083928, 3501384090, 1860741805, 220413581, 2875165616, 1234175883, 3889101468, 2248478338, 607754631, 3262521735, 1621405319, 4275622798, 2635233243, 995082921, 3649265332, 2008986042, 368322988, 3022937747, 1382148767, 4036851688, 2396396130, 755535591, 3410442795, 1769705866, 129003101, 2783109799, 1142777779, 3797013437, 2156543680, 516244057, 3170421230, 1530297929, 4184599293, 2544313925, 903527360, 3558092398, 1917426394, 276891186, 2931658296, 1290932783, 3944789534, 2304451464, 664264297, 3318248160, 1678177894, 37568937, 1091331878, 56, 404, 388, 390, 1726679070, 15, 12, 5, 3, 12, 16, 8, 11, 23, 21, 13, 18, 17, 17, 3, 16, 16, 15, 5, 8, 24, 23, 14, 23, 4, 24, 56, 423, 362, 369, 2112255272, 36, 10, 11, 1, 1, 8804, 1187956120, 34, 7, 13, 25, 17, 20, 10, 26, 5, 5, 6, 56, 284, 396, 397, 3164664852, 56, 416, 390, 392, 3472023858, 56, 390, 389, 405, 1816041616, 32, 10, 5, 6, 18, 0, 11, 4, 55, 11, 11, 12, 0, 56, 418, 347, 354, 1180176892, 4, 2, 56, 389, 359, 364, 763861455, 27, 17, 20, 19, 56, 385, 357, 362, 1143460340, 35, 1, 23, 17, 14, 18, 17, 56, 399, 230, 246, 3245231503, 3, 4283423324, 2308369661, 667625408, 3322389453, 1681534084, 40932480, 2695693823, 1054772403, 3708820637, 2068690843, 428157830, 3082345658, 1442215088, 4096521643, 2456247707, 815451361, 3470047078, 397727339, 3052292854, 1411513970, 4066200574, 2425804356, 784906968, 3439799872, 1799068100, 158364217, 2812448322]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 63: + { + // JUMP_114 (specialized) + regs[fp + SLOTS.PC] = 114; + break; + } + case 64: + { + // LOAD_CONST_4_4_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(4, 0); + break; + } + case 65: + { + // LOAD_CONST_5_5_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(5, 0); + break; + } + case 66: + { + // LOAD_CONST_6_6_0 (specialized) + var dst = 6; + regs[base + dst] = this._constant(6, 0); + break; + } + case 67: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 68: + { + // LOAD_CONST_7_7_0 (specialized) + var dst = 7; + regs[base + dst] = this._constant(7, 0); + break; + } + case 69: + { + // LOAD_GLOBAL_7_8_0 (specialized) + var dst = 7; + var globalName = this._constant(8, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 70: + { + // SET_PROP_7_8_9 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 7]; + var key = regs[base + 8]; + var val = regs[base + 9]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 71: + { + // RETURN_7 (specialized) + var retVal = regs[base + 7]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 72: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 73: + { + // FINALLY_SETUP_114_3_4_145 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 114, + contReg: 3, + payloadReg: 4, + throwPad: 145 + }); + break; + } + case 74: + { + // LOAD_INT_3_151 (specialized) + var dst = 3; + regs[base + dst] = 151; + break; + } + case 75: + { + // LOAD_INT_3_157 (specialized) + var dst = 3; + regs[base + dst] = 157; + break; + } + case 76: + { + // LOAD_UPVALUE_7_0 (specialized) + var dst = 7; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 77: + { + // STORE_UPVALUE_0_9 (specialized) + var uvIdx = 0; + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + 9]); + break; + } + case 78: + { + // JUMP_REG_3 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 3]; + break; + } + case 79: + { + // THROW_4 (specialized) + throw regs[base + 4]; + break; + } + case 80: + { + // RETURN_4 (specialized) + var retVal = regs[base + 4]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 81: + { + // RETURN_7 (specialized) + var retVal = regs[base + 7]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 82: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 83: + { + // ALIAS_LOAD_CONST_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 61: + { + // ANTI_MOVE_4_0_7_2_5_3_6_1 (order: [4,0,7,2,5,3,6,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[2]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // MOVE + var dst = _operands[6]; + regs[base + dst] = regs[base + _operands[7]]; + break; + } + case 62: + { + // ANTI_ADD_6_8_3_7_0_2_4_1_5 (order: [6,8,3,7,0,2,4,1,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[1]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[6]; + var name = this._constant(_operands[7], _operands[8]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/finally.src.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/finally.src.js new file mode 100644 index 00000000..e1662bd5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/finally.src.js @@ -0,0 +1 @@ +let count = 0; function check(value) { try { return value + 1; } finally { count++; } } window.TEST_OUTPUT = [check(4), count]; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/generation.json b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/generation.json new file mode 100644 index 00000000..8bf9141d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/generation.json @@ -0,0 +1,32 @@ +{ + "encoderPin": "20c5b96bf57337c568579352758d7685358650de", + "rng": { + "algorithm": "mulberry32", + "seed": 211 + }, + "toolchain": { + "node": "26.10.0", + "jdk": "27 (Homebrew)", + "closure": "20260216.0.0" + }, + "generator": "scripts/qualify-jsconfuser-vm/patch.mjs", + "profiles": { + "alias": "selfModifying+aliasedOpcodes", + "macro": "selfModifying+macroOpcodes", + "specialized": "selfModifying+specializedOpcodes", + "anti": "selfModifying+antiInstrumentation", + "stack": "selfModifying+aliasedOpcodes+macroOpcodes+specializedOpcodes+antiInstrumentation", + "finally": "selfModifying+aliasedOpcodes+macroOpcodes+specializedOpcodes+antiInstrumentation", + "arrays": "selfModifying+aliasedOpcodes+macroOpcodes+specializedOpcodes" + }, + "boundary": "Known-source numeric nonminified runtime, added-opcode/PATCH source and current native behavior/recovery/state; no universal semantics claim", + "cases": { + "alias": "arithmetic", + "macro": "arithmetic", + "specialized": "arithmetic", + "anti": "arithmetic", + "stack": "arithmetic", + "finally": "tryFinally", + "arrays": "patchArrays" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/macro.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/macro.js new file mode 100644 index 00000000..85b2b0ea --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/macro.js @@ -0,0 +1,1030 @@ +var CONSTANTS = [/* 0 */3, /* 1 */5, /* 2 */undefined, /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */6, /* 6 */7]; +var BYTECODE = [56, 5, 323, 340, 4121184879, 35, 18, 16, 26, 20, 10, 9, 21, 15, 13, 14, 25, 4, 24, 15, 4, 5, 56, 27, 216, 244, 354143194, 56, 144, 358, 374, 3494766079, 55, 14, 3, 20, 23, 36, 11, 0, 13, 1, 6, 21, 56, 158, 308, 314, 2002891106, 34, 10, 24, 5, 15, 15, 45, 2, 56, 303, 350, 355, 2165858660, 17, 18, 17, 0, 32, 24, 2, 22, 1, 19, 4, 17, 11, 21, 17, 14, 8, 22, 5, 56, 124, 362, 363, 1657700481, 22, 2, 10, 14, 56, 144, 341, 354, 2606459491, 11, 9, 10, 4, 34, 2, 5, 19, 2, 2, 16, 27, 11, 12, 5, 56, 301, 134, 145, 2585180553, 25, 13, 18, 19, 21, 3, 16, 10, 30, 4, 9, 55, 5, 23, 5, 56, 293, 293, 303, 228531296, 35, 0, 23, 13, 2, 12, 19, 36, 13, 8, 15, 5, 12, 8, 8, 12, 10, 15, 56, 342, 318, 319, 3745507913, 35, 18, 12, 56, 302, 134, 149, 1354207185, 50, 56, 302, 308, 322, 3952159008, 4, 24, 56, 297, 93, 100, 2112255272, 36, 10, 11, 4, 3, 32, 5, 19, 56, 200, 360, 371, 1392862651, 56, 201, 70, 83, 302753861, 17, 4, 16, 16, 18, 21, 19, 24, 21, 24, 13, 12, 5, 13, 10, 24, 0, 3, 16, 3008815085, 1367880544, 4022683385, 2382224749, 741363159, 3396269398, 1755536080, 114798389, 2768911153, 1128568522, 3782806850, 2142666181, 502011201, 3156316920, 1515798342, 4170490826, 2530105170, 889451223, 3544141239, 1903347131, 262716065, 2917478055, 1276500958, 3930613606, 2290281191, 650059128, 3304293867, 1663959701, 22, 18, 19, 17, 19, 22, 18, 5, 14, 21, 18, 9, 15, 5, 11, 22, 8, 3, 7, 17, 56, 389, 277, 283, 238713766, 32, 4, 21, 56, 344, 292, 307, 756360388, 8, 14, 3, 18, 8, 6, 13, 16, 26, 16, 8, 23, 1, 11, 47135, 21, 19, 14, 7, 27, 19, 18, 15, 19, 15, 24, 12, 56, 151, 138, 143, 3500334960, 18, 5, 5, 21, 14, 15, 6, 4, 22, 18, 11, 24, 55, 60, 2480888569, 840084804, 3494655426, 1853965661, 213503689, 2868147017, 1227544320, 3882296113, 2241051987, 600708572, 3254818157, 1614741244, 4268919652, 2628261871, 988134247, 3642567971, 2001790249, 34, 22, 15, 21, 8, 11, 2, 56, 298, 306, 313, 2730496771, 55, 19, 23, 20, 8, 15, 0, 24, 21, 23, 6, 5, 9, 21, 19, 23, 22, 21, 11, 8, 13, 17, 22, 20, 12, 13, 56, 127, 380, 387, 1180176892, 4, 2, 56, 297, 387, 392, 763861455, 27, 17, 20, 19, 56, 292, 386, 391, 1143460340, 21, 6, 1, 23]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/macro.src.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/macro.src.js new file mode 100644 index 00000000..e063dee6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/macro.src.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/specialized.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/specialized.js new file mode 100644 index 00000000..e4b7afcf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/specialized.js @@ -0,0 +1,1140 @@ +var CONSTANTS = [/* 0 */3, /* 1 */5, /* 2 */undefined, /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */6, /* 6 */7]; +var BYTECODE = [56, 5, 354, 359, 4121184879, 35, 18, 16, 4, 22, 56, 15, 29, 43, 3476235171, 12, 18, 17, 15, 18, 11, 21, 4, 19, 14, 5, 2, 55, 21, 1835660915, 195185229, 2849824351, 1209223704, 3863983625, 2222710268, 582377984, 3236492870, 1596232280, 4250537481, 2610004535, 969741884, 3624047155, 1983412819, 56, 44, 296, 312, 3364503553, 56, 261, 103, 111, 3461520427, 11, 22, 5, 5, 56, 290, 279, 289, 3999881398, 5, 10, 14, 56, 256, 280, 293, 2606459491, 11, 9, 10, 4, 34, 2, 5, 19, 2, 2, 16, 27, 11, 12, 5, 1, 11, 24823, 40, 23, 18, 20, 10, 56, 193, 269, 272, 2297122391, 15, 0, 7, 10, 56, 272, 104, 105, 1386881141, 26, 11, 7, 5, 55, 58, 11, 0, 22, 17, 32, 23, 1, 17, 4, 16, 16, 18, 21, 19, 24, 21, 24, 13, 12, 5, 13, 10, 55, 56, 73, 223, 229, 3389204423, 15, 15, 22, 18, 56, 207, 97, 111, 2425099559, 34, 11, 22, 8, 3, 7, 17, 56, 100, 150, 156, 238713766, 32, 4, 21, 55, 8, 21, 19, 14, 7, 27, 19, 18, 15, 19, 15, 24, 12, 56, 263, 250, 255, 3500334960, 18, 5, 5, 21, 14, 15, 6, 4, 22, 18, 11, 24, 55, 60, 18, 0, 11, 4, 55, 11, 11, 12, 0, 56, 169, 203, 210, 1180176892, 4, 2, 56, 92, 213, 218, 763861455, 27, 17, 20, 19, 56, 167, 211, 216, 1143460340, 35, 1, 23, 17, 14, 18, 17, 2, 4, 9, 55, 5, 23, 5, 56, 168, 168, 178, 228531296, 35, 0, 23, 13, 2, 12, 19, 36, 13, 8, 15, 5, 12, 8, 8, 12, 10, 15, 56, 281, 193, 194, 3745507913, 35, 18, 12, 56, 177, 246, 261, 1354207185, 50, 34, 22, 15, 21, 8, 11, 2, 56, 173, 181, 188, 2730496771, 55, 19, 23, 20, 8, 15, 0, 24, 21, 23, 6, 5, 9, 21, 19, 23, 22, 21, 11, 8, 13, 17, 22, 20, 12, 13, 18, 19, 21, 13, 23, 14, 17, 3, 13, 13, 15, 5, 15, 20, 23, 14, 15, 1, 24, 5, 23, 10, 14, 5, 20, 6, 10, 11, 4, 3, 32, 5, 19, 5, 24, 3, 39, 2480888512, 840084859, 3494655487, 1853965599, 213503627]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 62: + { + // LOAD_GLOBAL_2_3_0 (specialized) + var dst = 2; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 63: + { + // LOAD_CONST_3_4_0 (specialized) + var dst = 3; + regs[base + dst] = this._constant(4, 0); + break; + } + case 64: + { + // LOAD_CONST_4_5_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(5, 0); + break; + } + case 65: + { + // LOAD_CONST_5_6_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(6, 0); + break; + } + case 66: + { + // MUL_6_4_5 (specialized) + var dst = 6; + var a = regs[base + 4]; + regs[base + dst] = a * regs[base + 5]; + break; + } + case 67: + { + // LOAD_CONST_4_0_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(0, 0); + break; + } + case 68: + { + // ADD_5_6_4 (specialized) + var dst = 5; + var a = regs[base + 6]; + regs[base + dst] = a + regs[base + 4]; + break; + } + case 69: + { + // MOD_6_5_4 (specialized) + var dst = 6; + var a = regs[base + 5]; + regs[base + dst] = a % regs[base + 4]; + break; + } + case 70: + { + // SET_PROP_2_3_6 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 2]; + var key = regs[base + 3]; + var val = regs[base + 6]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 71: + { + // RETURN_2 (specialized) + var retVal = regs[base + 2]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/specialized.src.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/specialized.src.js new file mode 100644 index 00000000..e063dee6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/specialized.src.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/stack.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/stack.js new file mode 100644 index 00000000..59251e68 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/stack.js @@ -0,0 +1,1204 @@ +var CONSTANTS = [/* 0 */5, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */6, /* 4 */7, /* 5 */3, /* 6 */"GwUDm", /* 7 */undefined, /* 8 */62072, /* 9 */44867, /* 10 */"Dnqew1cI", /* 11 */14351, /* 12 */0]; +var BYTECODE = [56, 5, 324, 332, 2103690502, 56, 312, 107, 110, 494315154, 36, 3, 16, 56, 18, 173, 209, 3220258786, 11, 0, 22, 17, 32, 23, 1, 17, 4, 16, 16, 18, 21, 19, 24, 21, 24, 13, 12, 5, 13, 10, 55, 56, 134, 135, 141, 3389204423, 15, 15, 22, 18, 34, 15, 11, 50, 56, 59, 153, 173, 2089660694, 56, 382, 451, 460, 1295014798, 55, 8, 8, 19, 14, 56, 381, 314, 318, 592580987, 26, 0, 5, 4, 32, 56, 84, 269, 270, 4214031348, 33, 56, 90, 270, 275, 538041785, 14, 24, 6, 13, 38, 56, 280, 387, 400, 448859617, 14, 1, 2, 5, 26, 6, 11, 4, 56, 131, 443, 455, 761258985, 25, 6, 10, 0, 21, 6, 1, 23, 17, 14, 18, 17, 56, 136, 278, 294, 3245231503, 56, 433, 300, 305, 3672113915, 23, 10, 8, 22, 56, 303, 277, 278, 11546470, 27, 18, 2, 10, 5, 5, 0, 4, 1, 449182854, 3103351667, 1463199736, 4117508941, 2477231341, 836437340, 3491005871, 1850346685, 209755299, 2864523175, 1223535453, 3877657835, 2237330790, 597116413, 3251363176, 1611025556, 4265293714, 2624627599, 984247478, 3638949051, 1579829416, 4234661926, 2593356270, 953082021, 3607107770, 1966977974, 326436954, 2980610259, 1340536917, 3994775939, 2354518130, 713793739, 3368298562, 1727717372, 87115846, 2741878980, 1101158456, 3755028558, 2114762962, 474357849, 3128684526, 1488416882, 4142569700, 2501902372, 861780457, 3516207142, 1875404846, 235069446, 2889836595, 1249103889, 3903830037, 2263105555, 622254204, 3276370160, 1636165690, 4290355213, 16, 5, 16, 15, 56, 439, 296, 310, 2869808688, 34, 13, 5, 5, 1, 19, 56, 414, 379, 392, 2583648503, 35, 23, 7, 4, 22, 13, 20, 14, 0, 17, 12, 3, 5, 36, 20, 0, 27, 1, 18, 17, 32, 7, 21, 15, 23, 7, 11, 13, 14, 5, 13, 34, 22, 0, 5, 12, 9, 55, 4, 4, 2573252156, 3192694175, 1552037204, 4206862028, 2565538750, 925272062, 22, 14, 7, 11, 23, 18, 15, 15, 18, 24, 12, 3, 16, 6, 9, 17, 16, 14, 5, 5, 11, 10, 15, 6, 56, 447, 116, 129, 1885507148, 12, 9, 23, 16, 21, 16, 21, 22, 13, 9, 18, 17, 17, 14, 11, 21, 34, 24, 15, 35, 463200805, 3117905569, 1476981309, 4131789225, 2491254304, 850463451, 3505299026, 1864165947, 19, 8, 23, 9, 21, 14, 0, 11, 15, 17, 6, 22, 56, 366, 405, 409, 661359168, 12, 15, 12, 9, 27, 22, 5, 8, 24, 12, 7, 16, 27, 20, 11, 2, 35, 7, 9, 36, 8, 17, 21, 19, 0, 22, 4, 12, 10, 56, 315, 275, 282, 3377722290, 24, 10, 0, 13, 32, 20, 23, 36, 11, 0, 13, 1, 6, 21, 56, 143, 439, 445, 2002891106, 34, 10, 24, 19, 15, 0, 11, 56, 432, 288, 293, 2165858660, 18, 24, 8, 9, 32, 10, 5, 56, 279, 100, 111, 2874137787, 27, 11, 12, 5, 56, 390, 386, 397, 2585180553, 25, 13, 18, 19, 21, 3, 16, 10, 19, 5, 8, 12, 27, 14, 23, 11, 56, 429, 308, 316, 2431941071, 35, 12, 16, 60]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 64: + { + // LOAD_CONST_3_8_0 (specialized) + var dst = 3; + regs[base + dst] = this._constant(8, 0); + break; + } + case 65: + { + // LOAD_CONST_5_9_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(9, 0); + break; + } + case 66: + { + // LOAD_CONST_2_10_0 (specialized) + var dst = 2; + regs[base + dst] = this._constant(10, 0); + break; + } + case 67: + { + // LOAD_CONST_4_11_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(11, 0); + break; + } + case 68: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 69: + { + // LOAD_GLOBAL_7_1_0 (specialized) + var dst = 7; + var globalName = this._constant(1, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 70: + { + // SET_PROP_7_8_11 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 7]; + var key = regs[base + 8]; + var val = regs[base + 11]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 71: + { + // RETURN_7 (specialized) + var retVal = regs[base + 7]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 72: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 73: + { + // ALIAS_LOAD_CONST_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 61: + { + // ANTI_MUL_8_0_5_1_3_6_4_7_2 (order: [8,0,5,1,3,6,4,7,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[0]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + break; + } + case 62: + { + // ANTI_ADD_6_5_3_7_0_2_4_1 (order: [6,5,3,7,0,2,4,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[3]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // JUMP_IF_FALSE + var src = _operands[3]; + var target = _operands[4]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[5]; + var name = this._constant(_operands[6], _operands[7]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + case 63: + { + // ANTI_MOD_0_5_13_1_6_12_2_9_11_7_8_10_14_3_4 (order: [0,5,13,1,6,12,2,9,11,7,8,10,14,3,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[13], _unsortedOperands[14], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[12]]; + // MOD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a % regs[base + _operands[2]]; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // SHR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >> regs[base + _operands[8]]; + // EQ + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a === regs[base + _operands[11]]; + // ADD + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a + regs[base + _operands[14]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/stack.src.js b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/stack.src.js new file mode 100644 index 00000000..e063dee6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/patch-added-opcodes/stack.src.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/readability/arithmetic/decoded.js b/test/vm/jsconfuser-vm/fixtures/readability/arithmetic/decoded.js new file mode 100644 index 00000000..0ed27134 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/arithmetic/decoded.js @@ -0,0 +1,21 @@ +(function () { + 'use strict'; + function __recovered_function_0() { + let __recovered_r_0_1, __recovered_r_0_2, __recovered_r_0_3, __recovered_r_0_4; + __recovered_r_0_1 = this; + __recovered_r_0_2 = 4; + __recovered_r_0_3 = 2; + __recovered_r_0_4 = __recovered_r_0_2 > __recovered_r_0_3; + if (__recovered_r_0_4) { + __recovered_r_0_3 = 3; + __recovered_r_0_2 = __recovered_r_0_2 + __recovered_r_0_3; + } + if (!("window" in globalThis)) throw new ReferenceError("window" + ' is not defined'); + __recovered_r_0_3 = globalThis["window"]; + __recovered_r_0_4 = "TEST_OUTPUT"; + Reflect.set(__recovered_r_0_3, __recovered_r_0_4, __recovered_r_0_2); + __recovered_r_0_3 = void 0; + return __recovered_r_0_3; + } + __recovered_function_0(); +})(); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/readability/arithmetic/encoded.js b/test/vm/jsconfuser-vm/fixtures/readability/arithmetic/encoded.js new file mode 100644 index 00000000..d5c0f116 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/arithmetic/encoded.js @@ -0,0 +1,1035 @@ +// fn_0_0: +// [4, 1], LOAD_THIS reg[1] = this +// [0, 3, 0, 0], LOAD_CONST reg[3] = 4 1:12-1:13 +// [5, 2, 3], MOVE reg[2] = reg[3] 1:0-1:13 +// [0, 3, 1, 0], LOAD_CONST reg[3] = 2 2:12-2:13 +// [23, 4, 2, 3], GT reg[4] = reg[2] > reg[3] 2:4-2:13 +// [40, 4, 31], JUMP_IF_FALSE if (!reg[4]) goto if_else_1 2:0-4:1 +// [0, 3, 2, 0], LOAD_CONST reg[3] = 3 3:18-3:19 +// [11, 4, 2, 3], ADD reg[4] = reg[2] + reg[3] 3:10-3:19 +// [5, 2, 4], MOVE reg[2] = reg[4] 3:2-3:19 +// if_else_1: +// [2, 3, 3, 0], LOAD_GLOBAL reg[3] = window 5:0-5:6 +// [0, 4, 4, 0], LOAD_CONST reg[4] = "TEST_OUTPUT" 5:0-5:26 +// [9, 3, 4, 2], SET_PROP reg[3][reg[4]] = reg[2] 5:0-5:26 +// [0, 3, 5, 0], LOAD_CONST reg[3] = undefined +// [45, 3], RETURN reg[3] +var CONSTANTS = [/* 0 */4, /* 1 */2, /* 2 */3, /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */undefined]; +var BYTECODE = [4, 1, 0, 3, 0, 0, 5, 2, 3, 0, 3, 1, 0, 23, 4, 2, 3, 40, 4, 31, 0, 3, 2, 0, 11, 4, 2, 3, 5, 2, 4, 2, 3, 3, 0, 0, 4, 4, 0, 9, 3, 4, 2, 0, 3, 5, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 5; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/readability/arithmetic/source.js b/test/vm/jsconfuser-vm/fixtures/readability/arithmetic/source.js new file mode 100644 index 00000000..9f9d5e11 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/arithmetic/source.js @@ -0,0 +1,5 @@ +var value = 4 +if (value > 2) { + value = value + 3 +} +window.TEST_OUTPUT = value diff --git a/test/vm/jsconfuser-vm/fixtures/readability/baseline.json b/test/vm/jsconfuser-vm/fixtures/readability/baseline.json new file mode 100644 index 00000000..e4455b6a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/baseline.json @@ -0,0 +1,116 @@ +{ + "schemaVersion": "jsconfuser-vm-readability-baseline.v1", + "fixtureSchemaVersion": "jsconfuser-vm-readability-fixtures.v1", + "cases": [ + { + "caseId": "arithmetic", + "decoded": { + "bytes": 1668, + "sha256": "1a294dd46b54b309f49a29b383aeec615ac1e8310764270f411ce4a67f05b8fc", + "ratios": { + "decodedToSource": 20.85, + "decodedToEncoded": 0.042799 + }, + "ast": { + "statements": 45, + "generatedStatements": 23, + "functions": 3, + "declaredIdentifiers": 16, + "generatedDeclarations": { + "total": 10, + "zeroReferences": 6 + }, + "emptyCellDeclarations": 1 + }, + "generatedIdentifiers": { + "distinct": 10, + "total": 33 + }, + "operations": { + "reflectCalls": { + "set": 1 + }, + "computedGlobalThis": 0, + "descriptorCalls": 0, + "closureWrappers": 0, + "handlerOperations": 0, + "structuredFunctions": 1 + } + } + }, + { + "caseId": "closure-loop", + "decoded": { + "bytes": 4255, + "sha256": "d682fc02d56aaab3d30c4d643d3a0cc0d325402e08b37c6d4077cc0d7d68028b", + "ratios": { + "decodedToSource": 19.080717, + "decodedToEncoded": 0.103139 + }, + "ast": { + "statements": 101, + "generatedStatements": 77, + "functions": 7, + "declaredIdentifiers": 43, + "generatedDeclarations": { + "total": 36, + "zeroReferences": 12 + }, + "emptyCellDeclarations": 3 + }, + "generatedIdentifiers": { + "distinct": 31, + "total": 116 + }, + "operations": { + "reflectCalls": { + "apply": 2, + "set": 1 + }, + "computedGlobalThis": 0, + "descriptorCalls": 0, + "closureWrappers": 3, + "handlerOperations": 0, + "structuredFunctions": 3 + } + } + }, + { + "caseId": "scale", + "decoded": { + "bytes": 15367, + "sha256": "55b103cf42fb22f4d447c7286330512e26c718f79c3345d3a144f4c40659aa1d", + "ratios": { + "decodedToSource": 7.970436, + "decodedToEncoded": 0.269634 + }, + "ast": { + "statements": 240, + "generatedStatements": 218, + "functions": 5, + "declaredIdentifiers": 30, + "generatedDeclarations": { + "total": 24, + "zeroReferences": 10 + }, + "emptyCellDeclarations": 2 + }, + "generatedIdentifiers": { + "distinct": 22, + "total": 509 + }, + "operations": { + "reflectCalls": { + "apply": 1, + "set": 1 + }, + "computedGlobalThis": 0, + "descriptorCalls": 0, + "closureWrappers": 2, + "handlerOperations": 0, + "structuredFunctions": 2 + } + } + } + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/readability/closure-loop/decoded.js b/test/vm/jsconfuser-vm/fixtures/readability/closure-loop/decoded.js new file mode 100644 index 00000000..efc64c81 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/closure-loop/decoded.js @@ -0,0 +1,59 @@ +(function () { + 'use strict'; + function __recovered_function_0() { + let __recovered_r_0_1, __recovered_r_0_2, __recovered_r_0_3, __recovered_r_0_4, __recovered_r_0_5, __recovered_r_0_6, __recovered_r_0_7; + __recovered_r_0_1 = this; + __recovered_r_0_2 = function (...__recovered_closure_args) { const __recovered_this = this == null ? globalThis : this; return Reflect.apply(__recovered_function_1, __recovered_this, [...__recovered_closure_args]); }; + __recovered_r_0_6 = 4; + __recovered_r_0_7 = __recovered_r_0_2(__recovered_r_0_6); + __recovered_r_0_3 = __recovered_r_0_7; + __recovered_r_0_4 = 0; + __recovered_r_0_5 = 0; + __recovered_loop_0_0: while (true) { + __recovered_r_0_6 = 3; + __recovered_r_0_7 = __recovered_r_0_5 < __recovered_r_0_6; + if (__recovered_r_0_7) { + __recovered_r_0_6 = __recovered_r_0_3(__recovered_r_0_5); + __recovered_r_0_4 = __recovered_r_0_4 + __recovered_r_0_6; + __recovered_r_0_6 = __recovered_r_0_5; + __recovered_r_0_6 = 1; + __recovered_r_0_5 = __recovered_r_0_5 + __recovered_r_0_6; + continue __recovered_loop_0_0; + } + break __recovered_loop_0_0; + } + if (!("window" in globalThis)) throw new ReferenceError("window" + ' is not defined'); + __recovered_r_0_6 = globalThis["window"]; + __recovered_r_0_7 = "TEST_OUTPUT"; + Reflect.set(__recovered_r_0_6, __recovered_r_0_7, __recovered_r_0_4); + __recovered_r_0_6 = void 0; + return __recovered_r_0_6; + } + function __recovered_function_1(...__recovered_args) { + const __recovered_cells_1 = []; + let __recovered_r_1_0, __recovered_r_1_2, __recovered_r_1_3; + __recovered_r_1_0 = __recovered_args[0]; + __recovered_r_1_2 = this; + __recovered_cells_1[0] ||= { get value() { return __recovered_r_1_0; }, set value(__value) { __recovered_r_1_0 = __value; } }; + const __recovered_closure_captures_1_97 = [__recovered_cells_1[0]]; + __recovered_r_1_3 = function (...__recovered_closure_args) { const __recovered_this = this == null ? globalThis : this; return Reflect.apply(__recovered_function_2, __recovered_this, [__recovered_closure_captures_1_97, ...__recovered_closure_args]); }; + return __recovered_r_1_3; + if (false) { + __recovered_r_1_3 = void 0; + void 0; + } + } + function __recovered_function_2(__recovered_captures, ...__recovered_args) { + let __recovered_r_2_0, __recovered_r_2_2, __recovered_r_2_3, __recovered_r_2_4; + __recovered_r_2_0 = __recovered_args[0]; + __recovered_r_2_2 = this; + __recovered_r_2_3 = __recovered_captures[0].value; + __recovered_r_2_4 = __recovered_r_2_3 + __recovered_r_2_0; + return __recovered_r_2_4; + if (false) { + __recovered_r_2_3 = void 0; + void 0; + } + } + __recovered_function_0(); +})(); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/readability/closure-loop/encoded.js b/test/vm/jsconfuser-vm/fixtures/readability/closure-loop/encoded.js new file mode 100644 index 00000000..2b6ba389 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/closure-loop/encoded.js @@ -0,0 +1,1062 @@ +// fn_0_0: +// [4, 1], LOAD_THIS reg[1] = this +// [47, 6, 95, 1, 4, 0, 0], MAKE_CLOSURE reg[6] PC=fn_1_1 (params=1 regs=4 upvalues=0) 1:0-5:1 +// [5, 2, 6], MOVE reg[2] = reg[6] 1:0-5:1 +// [0, 6, 0, 0], LOAD_CONST reg[6] = 4 7:20-7:21 +// [42, 7, 2, 1, 6], CALL reg[7] = reg[2](reg[6]) 7:10-7:22 +// [5, 3, 7], MOVE reg[3] = reg[7] 7:0-7:22 +// [0, 6, 1, 0], LOAD_CONST reg[6] = 0 8:12-8:13 +// [5, 4, 6], MOVE reg[4] = reg[6] 8:0-8:13 +// [0, 6, 1, 0], LOAD_CONST reg[6] = 0 9:17-9:18 +// [5, 5, 6], MOVE reg[5] = reg[6] 9:5-9:18 +// for_top_3: +// [0, 6, 2, 0], LOAD_CONST reg[6] = 3 9:28-9:29 +// [22, 7, 5, 6], LT reg[7] = reg[5] < reg[6] 9:20-9:29 +// [40, 7, 77], JUMP_IF_FALSE if (!reg[7]) goto for_exit_4 9:0-11:1 +// [42, 6, 3, 1, 5], CALL reg[6] = reg[3](reg[5]) 10:18-10:28 +// [11, 7, 4, 6], ADD reg[7] = reg[4] + reg[6] 10:10-10:28 +// [5, 4, 7], MOVE reg[4] = reg[7] 10:2-10:28 +// for_update_5: +// [5, 6, 5], MOVE reg[6] = reg[5] 9:31-9:38 +// [0, 6, 3, 0], LOAD_CONST reg[6] = 1 9:31-9:38 +// [11, 7, 5, 6], ADD reg[7] = reg[5] + reg[6] 9:31-9:38 +// [5, 5, 7], MOVE reg[5] = reg[7] 9:31-9:38 +// [39, 38], JUMP goto for_top_3 9:0-11:1 +// for_exit_4: +// [2, 6, 4, 0], LOAD_GLOBAL reg[6] = window 12:0-12:6 +// [0, 7, 5, 0], LOAD_CONST reg[7] = "TEST_OUTPUT" 12:0-12:26 +// [9, 6, 7, 4], SET_PROP reg[6][reg[7]] = reg[4] 12:0-12:26 +// [0, 6, 6, 0], LOAD_CONST reg[6] = undefined +// [45, 6], RETURN reg[6] +// fn_1_1: +// [4, 2], LOAD_THIS reg[2] = this 1:0-5:1 +// [47, 3, 114, 1, 5, 1, 0, 1, 0], MAKE_CLOSURE reg[3] PC=fn_2_2 (params=1 regs=5 upvalues=1) 2:9-4:3 +// [45, 3], RETURN reg[3] 2:2-4:3 +// [0, 3, 6, 0], LOAD_CONST reg[3] = undefined 1:0-5:1 +// [45, 3], RETURN reg[3] 1:0-5:1 +// fn_2_2: +// [4, 2], LOAD_THIS reg[2] = this 2:9-4:3 +// [3, 3, 0], LOAD_UPVALUE reg[3] = upvalue[0] 3:11-3:15 +// [11, 4, 3, 0], ADD reg[4] = reg[3] + reg[0] 3:11-3:23 +// [45, 4], RETURN reg[4] 3:4-3:23 +// [0, 3, 6, 0], LOAD_CONST reg[3] = undefined 2:9-4:3 +// [45, 3], RETURN reg[3] 2:9-4:3 +var CONSTANTS = [/* 0 */4, /* 1 */0, /* 2 */3, /* 3 */1, /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */undefined]; +var BYTECODE = [4, 1, 47, 6, 95, 1, 4, 0, 0, 5, 2, 6, 0, 6, 0, 0, 42, 7, 2, 1, 6, 5, 3, 7, 0, 6, 1, 0, 5, 4, 6, 0, 6, 1, 0, 5, 5, 6, 0, 6, 2, 0, 22, 7, 5, 6, 40, 7, 77, 42, 6, 3, 1, 5, 11, 7, 4, 6, 5, 4, 7, 5, 6, 5, 0, 6, 3, 0, 11, 7, 5, 6, 5, 5, 7, 39, 38, 2, 6, 4, 0, 0, 7, 5, 0, 9, 6, 7, 4, 0, 6, 6, 0, 45, 6, 4, 2, 47, 3, 114, 1, 5, 1, 0, 1, 0, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 3, 3, 0, 11, 4, 3, 0, 45, 4, 0, 3, 6, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/readability/closure-loop/source.js b/test/vm/jsconfuser-vm/fixtures/readability/closure-loop/source.js new file mode 100644 index 00000000..53761f47 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/closure-loop/source.js @@ -0,0 +1,12 @@ +function makeAdder(base) { + return function (value) { + return base + value + } +} + +var add = makeAdder(4) +var total = 0 +for (var index = 0; index < 3; index++) { + total = total + add(index) +} +window.TEST_OUTPUT = total diff --git a/test/vm/jsconfuser-vm/fixtures/readability/manifest.json b/test/vm/jsconfuser-vm/fixtures/readability/manifest.json new file mode 100644 index 00000000..85102232 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/manifest.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": "jsconfuser-vm-readability-fixtures.v1", + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "options": { + "target": "browser", + "encodeBytecode": false, + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false + }, + "cases": [ + "arithmetic", + "closure-loop", + "scale" + ], + "generatedInputs": "encoded.js is generated from each tracked source.js with the pinned encoder and the options above.", + "baseline": "decoded.js is the current decoder output and is measured, not treated as a readability target." +} diff --git a/test/vm/jsconfuser-vm/fixtures/readability/scale/decoded.js b/test/vm/jsconfuser-vm/fixtures/readability/scale/decoded.js new file mode 100644 index 00000000..0dbfc38e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/scale/decoded.js @@ -0,0 +1,209 @@ +(function () { + 'use strict'; + function __recovered_function_0() { + let __recovered_r_0_1, __recovered_r_0_2, __recovered_r_0_3, __recovered_r_0_4, __recovered_r_0_5; + __recovered_r_0_1 = this; + __recovered_r_0_2 = function (...__recovered_closure_args) { const __recovered_this = this == null ? globalThis : this; return Reflect.apply(__recovered_function_1, __recovered_this, [...__recovered_closure_args]); }; + __recovered_r_0_3 = 0; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + __recovered_r_0_4 = __recovered_r_0_2(__recovered_r_0_3); + __recovered_r_0_3 = __recovered_r_0_4; + if (!("window" in globalThis)) throw new ReferenceError("window" + ' is not defined'); + __recovered_r_0_4 = globalThis["window"]; + __recovered_r_0_5 = "TEST_OUTPUT"; + Reflect.set(__recovered_r_0_4, __recovered_r_0_5, __recovered_r_0_3); + __recovered_r_0_4 = void 0; + return __recovered_r_0_4; + } + function __recovered_function_1(...__recovered_args) { + let __recovered_r_1_0, __recovered_r_1_2, __recovered_r_1_3, __recovered_r_1_4; + __recovered_r_1_0 = __recovered_args[0]; + __recovered_r_1_2 = this; + __recovered_r_1_3 = __recovered_r_1_0; + __recovered_r_1_4 = 1; + __recovered_r_1_3 = __recovered_r_1_3 + __recovered_r_1_4; + __recovered_r_1_4 = 2; + __recovered_r_1_3 = __recovered_r_1_3 * __recovered_r_1_4; + return __recovered_r_1_3; + if (false) { + __recovered_r_1_4 = void 0; + void 0; + } + } + __recovered_function_0(); +})(); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/readability/scale/encoded.js b/test/vm/jsconfuser-vm/fixtures/readability/scale/encoded.js new file mode 100644 index 00000000..176bcebe --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/scale/encoded.js @@ -0,0 +1,1220 @@ +// fn_0_0: +// [4, 1], LOAD_THIS reg[1] = this +// [47, 4, 749, 1, 6, 0, 0], MAKE_CLOSURE reg[4] PC=fn_1_1 (params=1 regs=6 upvalues=0) 1:0-6:1 +// [5, 2, 4], MOVE reg[2] = reg[4] 1:0-6:1 +// [0, 4, 0, 0], LOAD_CONST reg[4] = 0 8:12-8:13 +// [5, 3, 4], MOVE reg[3] = reg[4] 8:0-8:13 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 9:8-9:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 9:0-9:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 10:8-10:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 10:0-10:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 11:8-11:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 11:0-11:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 12:8-12:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 12:0-12:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 13:8-13:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 13:0-13:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 14:8-14:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 14:0-14:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 15:8-15:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 15:0-15:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 16:8-16:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 16:0-16:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 17:8-17:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 17:0-17:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 18:8-18:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 18:0-18:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 19:8-19:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 19:0-19:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 20:8-20:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 20:0-20:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 21:8-21:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 21:0-21:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 22:8-22:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 22:0-22:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 23:8-23:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 23:0-23:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 24:8-24:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 24:0-24:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 25:8-25:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 25:0-25:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 26:8-26:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 26:0-26:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 27:8-27:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 27:0-27:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 28:8-28:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 28:0-28:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 29:8-29:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 29:0-29:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 30:8-30:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 30:0-30:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 31:8-31:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 31:0-31:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 32:8-32:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 32:0-32:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 33:8-33:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 33:0-33:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 34:8-34:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 34:0-34:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 35:8-35:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 35:0-35:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 36:8-36:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 36:0-36:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 37:8-37:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 37:0-37:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 38:8-38:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 38:0-38:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 39:8-39:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 39:0-39:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 40:8-40:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 40:0-40:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 41:8-41:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 41:0-41:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 42:8-42:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 42:0-42:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 43:8-43:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 43:0-43:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 44:8-44:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 44:0-44:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 45:8-45:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 45:0-45:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 46:8-46:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 46:0-46:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 47:8-47:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 47:0-47:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 48:8-48:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 48:0-48:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 49:8-49:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 49:0-49:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 50:8-50:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 50:0-50:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 51:8-51:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 51:0-51:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 52:8-52:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 52:0-52:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 53:8-53:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 53:0-53:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 54:8-54:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 54:0-54:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 55:8-55:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 55:0-55:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 56:8-56:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 56:0-56:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 57:8-57:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 57:0-57:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 58:8-58:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 58:0-58:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 59:8-59:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 59:0-59:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 60:8-60:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 60:0-60:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 61:8-61:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 61:0-61:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 62:8-62:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 62:0-62:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 63:8-63:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 63:0-63:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 64:8-64:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 64:0-64:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 65:8-65:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 65:0-65:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 66:8-66:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 66:0-66:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 67:8-67:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 67:0-67:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 68:8-68:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 68:0-68:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 69:8-69:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 69:0-69:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 70:8-70:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 70:0-70:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 71:8-71:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 71:0-71:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 72:8-72:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 72:0-72:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 73:8-73:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 73:0-73:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 74:8-74:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 74:0-74:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 75:8-75:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 75:0-75:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 76:8-76:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 76:0-76:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 77:8-77:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 77:0-77:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 78:8-78:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 78:0-78:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 79:8-79:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 79:0-79:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 80:8-80:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 80:0-80:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 81:8-81:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 81:0-81:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 82:8-82:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 82:0-82:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 83:8-83:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 83:0-83:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 84:8-84:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 84:0-84:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 85:8-85:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 85:0-85:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 86:8-86:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 86:0-86:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 87:8-87:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 87:0-87:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 88:8-88:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 88:0-88:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 89:8-89:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 89:0-89:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 90:8-90:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 90:0-90:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 91:8-91:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 91:0-91:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 92:8-92:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 92:0-92:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 93:8-93:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 93:0-93:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 94:8-94:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 94:0-94:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 95:8-95:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 95:0-95:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 96:8-96:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 96:0-96:19 +// [42, 4, 2, 1, 3], CALL reg[4] = reg[2](reg[3]) 97:8-97:19 +// [5, 3, 4], MOVE reg[3] = reg[4] 97:0-97:19 +// [2, 4, 1, 0], LOAD_GLOBAL reg[4] = window 98:0-98:6 +// [0, 5, 2, 0], LOAD_CONST reg[5] = "TEST_OUTPUT" 98:0-98:26 +// [9, 4, 5, 3], SET_PROP reg[4][reg[5]] = reg[3] 98:0-98:26 +// [0, 4, 3, 0], LOAD_CONST reg[4] = undefined +// [45, 4], RETURN reg[4] +// fn_1_1: +// [4, 2], LOAD_THIS reg[2] = this 1:0-6:1 +// [5, 3, 0], MOVE reg[3] = reg[0] 2:2-2:20 +// [0, 4, 4, 0], LOAD_CONST reg[4] = 1 3:20-3:21 +// [11, 5, 3, 4], ADD reg[5] = reg[3] + reg[4] 3:11-3:21 +// [5, 3, 5], MOVE reg[3] = reg[5] 3:2-3:21 +// [0, 4, 5, 0], LOAD_CONST reg[4] = 2 4:20-4:21 +// [13, 5, 3, 4], MUL reg[5] = reg[3] * reg[4] 4:11-4:21 +// [5, 3, 5], MOVE reg[3] = reg[5] 4:2-4:21 +// [45, 3], RETURN reg[3] 5:2-5:15 +// [0, 4, 3, 0], LOAD_CONST reg[4] = undefined 1:0-6:1 +// [45, 4], RETURN reg[4] 1:0-6:1 +var CONSTANTS = [/* 0 */0, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */undefined, /* 4 */1, /* 5 */2]; +var BYTECODE = [4, 1, 47, 4, 749, 1, 6, 0, 0, 5, 2, 4, 0, 4, 0, 0, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 42, 4, 2, 1, 3, 5, 3, 4, 2, 4, 1, 0, 0, 5, 2, 0, 9, 4, 5, 3, 0, 4, 3, 0, 45, 4, 4, 2, 5, 3, 0, 0, 4, 4, 0, 11, 5, 3, 4, 5, 3, 5, 0, 4, 5, 0, 13, 5, 3, 4, 5, 3, 5, 45, 3, 0, 4, 3, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 6; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/readability/scale/source.js b/test/vm/jsconfuser-vm/fixtures/readability/scale/source.js new file mode 100644 index 00000000..801fdf98 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/readability/scale/source.js @@ -0,0 +1,98 @@ +function step(value) { + var result = value + result = result + 1 + result = result * 2 + return result +} + +var total = 0 +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +total = step(total) +window.TEST_OUTPUT = total diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/arithmetic-alias-specialized-212.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/arithmetic-alias-specialized-212.encoded.js new file mode 100644 index 00000000..0ad4ca12 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/arithmetic-alias-specialized-212.encoded.js @@ -0,0 +1,1149 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = [56, 5, 124, 130, 566760678, 56, 202, 91, 102, 2452072598, 44, 56, 16, 352, 365, 1862545045, 12, 20, 11, 9, 1, 13, 33527, 27, 22, 5, 2, 4, 14, 15, 14, 22, 17, 20, 5, 0, 6, 8, 1, 21, 17, 36, 20, 4, 24, 21, 7, 13, 56, 34, 283, 293, 3705054568, 56, 84, 188, 192, 1086901500, 13, 7, 8, 3, 19, 7, 2, 24, 21, 5, 13, 8, 11, 20, 6, 5, 36, 17, 20, 36, 56, 84, 46, 50, 1662846016, 56, 201, 103, 109, 1944769562, 27, 1, 9, 0, 15, 0, 6, 10, 25, 22, 18, 14, 22, 3, 8, 20, 32, 11, 10, 27, 3, 24, 21, 56, 56, 67, 75, 871114832, 14, 17, 10, 20, 32, 6, 0, 0, 3220861277, 1580595667, 4234764613, 2594254504, 953987882, 3608303580, 1, 3, 21674, 26, 9, 11, 16, 56, 58, 259, 274, 2821588635, 1, 6, 28826, 18, 16, 1, 16, 22, 21, 15, 18, 11, 23, 22, 18, 36, 2, 22, 56, 275, 111, 123, 3920705561, 23, 13, 10, 8, 23, 2, 23, 15, 18, 24, 11, 15, 16, 7, 14, 5, 19, 9, 9, 4, 17, 8, 22, 22, 18, 4, 16, 12, 16, 15, 23, 36, 17, 11, 13, 13, 11, 13, 11, 14, 20, 9, 27, 18, 0, 9, 35, 6, 15, 13, 10, 18, 1, 56, 201, 276, 284, 3590232693, 55, 25, 11, 3, 24, 25, 8, 20, 18, 15, 14, 10, 14, 5, 2, 23, 55, 56, 290, 294, 295, 1026936524, 56, 80, 249, 262, 1464376579, 56, 253, 100, 113, 2536925962, 23, 1, 5, 1, 26, 6, 23, 12, 26, 14, 11, 18, 21, 1, 5, 17, 5, 7, 10, 23, 3, 9, 10, 56, 95, 197, 213, 1225638588, 56, 276, 298, 299, 2115673432, 56, 110, 73, 77, 4151471928, 25, 5, 20, 13, 55, 44, 24, 21, 3, 6, 4, 19, 20, 19, 4, 1, 14, 18, 22, 0, 34, 13, 1, 16, 18, 1, 10, 36, 7, 15, 18, 0, 0, 11, 14, 8, 9, 6, 20, 23, 15, 17, 18, 24, 13, 16, 8, 19, 23, 18, 56, 329, 98, 111, 3602493732, 8, 2, 3, 6, 222020000, 2876668458, 1235801772, 3890555517, 2250096622, 609233494, 3264124565, 1622997492, 4277174773, 2636777162, 996651595, 3650826200, 2010571271]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 62: + { + // LOAD_GLOBAL_2_0_0 (specialized) + var dst = 2; + var globalName = this._constant(0, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 63: + { + // LOAD_CONST_3_1_0 (specialized) + var dst = 3; + regs[base + dst] = this._constant(1, 0); + break; + } + case 64: + { + // LOAD_CONST_4_2_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(2, 0); + break; + } + case 65: + { + // LOAD_CONST_5_3_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(3, 0); + break; + } + case 66: + { + // MUL_6_4_5 (specialized) + var dst = 6; + var a = regs[base + 4]; + regs[base + dst] = a * regs[base + 5]; + break; + } + case 67: + { + // LOAD_CONST_4_4_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(4, 0); + break; + } + case 68: + { + // ADD_5_6_4 (specialized) + var dst = 5; + var a = regs[base + 6]; + regs[base + dst] = a + regs[base + 4]; + break; + } + case 69: + { + // MOD_6_5_4 (specialized) + var dst = 6; + var a = regs[base + 5]; + regs[base + dst] = a % regs[base + 4]; + break; + } + case 70: + { + // SET_PROP_2_3_6 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 2]; + var key = regs[base + 3]; + var val = regs[base + 6]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 71: + { + // RETURN_2 (specialized) + var retVal = regs[base + 2]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 72: + { + // ALIAS_LOAD_CONST_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/arithmetic.src.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/arithmetic.src.js new file mode 100644 index 00000000..e063dee6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/arithmetic.src.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/cff-anti-2.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/cff-anti-2.encoded.js new file mode 100644 index 00000000..8c9323ee --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/cff-anti-2.encoded.js @@ -0,0 +1,1168 @@ +var CONSTANTS = [/* 0 */41342, /* 1 */17930, /* 2 */"undefined", /* 3 */2113, /* 4 */"Infinity", /* 5 */19507, /* 6 */"isNaN", /* 7 */34911, /* 8 */12864, /* 9 */"JSON", /* 10 */22107, /* 11 */38665, /* 12 */"Boolean", /* 13 */1, /* 14 */"RegExp", /* 15 */"Z1JP", /* 16 */0, /* 17 */undefined, /* 18 */"pn7", /* 19 */51222, /* 20 */11416, /* 21 */"window", /* 22 */"xzY3uA35", /* 23 */20625, /* 24 */"ixtit3qr", /* 25 */56166, /* 26 */"Wx1dPQ", /* 27 */44244, /* 28 */48119, /* 29 */"TEST_OUTPUT", /* 30 */4]; +var BYTECODE = [56, 5, 1013, 1025, 138668148, 8, 3, 16, 11, 12, 11, 19, 1, 15, 6, 19, 0, 56, 22, 991, 1013, 2062093430, 56, 905, 953, 955, 2227414787, 17, 4, 0, 19, 56, 880, 940, 954, 157389284, 16, 19, 22, 13, 21, 23, 23, 21, 56, 49, 1144, 1165, 1516068587, 56, 919, 883, 892, 2015137540, 12, 14, 23, 1, 56, 968, 907, 914, 1108984185, 26, 17, 4, 21, 5, 17, 6, 45, 6, 17, 20, 24, 5, 15, 18, 21, 10, 56, 85, 1025, 1069, 900605278, 19, 22, 7, 20, 20, 22, 0, 13, 15, 2, 16, 8, 18, 16, 8, 16, 18, 23, 0, 8, 56, 972, 967, 968, 541686133, 24, 0, 6, 1, 25, 10, 16, 18, 32, 11, 11, 56, 966, 1089, 1095, 800754151, 1, 22, 50427, 56, 134, 850, 868, 3491641410, 22, 22, 8, 19, 56, 1108, 940, 956, 4229242187, 18, 21, 17, 5, 23, 14, 4, 19, 54, 40, 12, 548, 0, 13, 0, 0, 61, 41, 40, 41, 40, 13, 40, 41, 10, 63, 14, 41, 40, 41, 8, 41, 2, 40, 40, 10, 40, 41, 40, 14, 186, 39, 752, 0, 15, 1, 0, 64, 2, 40, 40, 10, 16, 40, 0, 15, 40, 61, 40, 40, 40, 41, 16, 40, 40, 10, 56, 214, 1131, 1144, 1903028716, 56, 877, 888, 902, 390052414, 19, 20, 0, 22, 15, 12, 21, 11, 40, 17, 237, 39, 641, 56, 1116, 885, 894, 2740201059, 0, 18, 3, 0, 64, 4, 41, 41, 10, 19, 41, 0, 18, 40, 61, 41, 40, 40, 40, 19, 41, 40, 10, 63, 20, 40, 40, 40, 8, 40, 0, 40, 41, 10, 40, 40, 40, 20, 278, 39, 589, 56, 283, 868, 872, 159851854, 34, 5, 3, 50, 65, 21208, 10, 40, 41, 41, 41, 21, 40, 41, 40, 40, 22, 61, 40, 41, 40, 41, 22, 41, 40, 10, 63, 23, 41, 40, 40, 8, 41, 1, 41, 41, 10, 41, 41, 40, 23, 332, 39, 720, 56, 895, 1092, 1104, 655329445, 0, 24, 5, 0, 64, 6, 41, 40, 10, 25, 40, 0, 24, 41, 61, 40, 41, 40, 40, 25, 40, 40, 10, 63, 26, 41, 40, 40, 8, 40, 2, 40, 40, 10, 40, 41, 40, 26, 373, 39, 792, 0, 27, 7, 0, 65, 64603, 10, 40, 41, 41, 40, 27, 40, 41, 41, 41, 28, 61, 40, 40, 41, 40, 28, 41, 41, 10, 63, 29, 41, 41, 40, 8, 41, 1, 41, 41, 10, 41, 41, 40, 29, 420, 39, 758, 1, 18, 51832, 0, 30, 8, 0, 64, 9, 40, 40, 10, 31, 41, 0, 30, 40, 61, 40, 41, 40, 40, 31, 40, 41, 10, 63, 32, 41, 41, 41, 8, 40, 2, 41, 41, 10, 40, 41, 40, 32, 461, 39, 548, 0, 33, 10, 0, 65, 61880, 10, 40, 40, 41, 41, 33, 40, 41, 41, 41, 34, 61, 41, 40, 40, 40, 34, 41, 40, 10, 63, 35, 41, 41, 40, 8, 41, 1, 41, 41, 10, 40, 40, 40, 35, 505, 39, 718, 0, 36, 11, 0, 64, 12, 40, 40, 10, 37, 41, 0, 36, 41, 61, 40, 40, 41, 41, 37, 40, 40, 10, 63, 38, 40, 41, 40, 8, 41, 0, 40, 41, 10, 40, 40, 40, 38, 546, 39, 674, 39, 129, 61, 40, 41, 40, 41, 4, 40, 40, 44, 0, 44, 13, 0, 65, 8212, 4, 40, 40, 40, 40, 44, 41, 41, 41, 40, 5, 1, 39, 11843, 64, 14, 41, 41, 8, 8, 40, 0, 39, 41, 39, 129, 66, 4, 41, 0, 41, 41, 41, 41, 15, 41, 41, 44, 0, 41, 41, 41, 41, 44, 626, 1, 39, 36063, 65, 2033, 8, 41, 40, 40, 41, 39, 41, 40, 41, 40, 8, 39, 129, 1, 39, 2602, 64, 4, 40, 40, 8, 8, 40, 0, 39, 40, 39, 129, 61, 40, 41, 41, 41, 5, 40, 40, 4, 1, 39, 2113, 64, 4, 41, 40, 8, 8, 40, 0, 39, 40, 39, 129, 56, 973, 1084, 1089, 1437323995, 16, 11, 17, 7, 65, 4865, 3, 41, 41, 40, 40, 4, 41, 41, 40, 40, 44, 61, 41, 40, 40, 41, 44, 40, 40, 3, 1, 39, 16558, 65, 58554, 8, 41, 40, 40, 40, 39, 41, 41, 40, 40, 8, 39, 129, 20, 1, 7, 16, 45, 3, 0, 44, 16, 0, 61, 40, 40, 40, 40, 44, 41, 40, 4, 1, 39, 11416, 64, 6, 40, 40, 8, 8, 40, 0, 39, 40, 39, 129, 21, 4, 18, 8, 0, 44, 17, 0, 45, 44, 4, 2, 0, 44, 16, 0, 61, 40, 40, 40, 41, 44, 41, 40, 3, 1, 39, 15404, 64, 2, 41, 41, 8, 8, 41, 0, 39, 41, 39, 129, 23, 18, 7, 0, 1, 39, 15256, 66, 8, 41, 39, 40, 41, 41, 41, 18, 41, 41, 39, 0, 41, 40, 40, 41, 39, 832, 1, 39, 44154, 65, 40623, 8, 40, 40, 40, 41, 39, 41, 41, 40, 41, 8, 39, 129, 1, 39, 5489, 65, 35518, 8, 41, 40, 41, 41, 39, 41, 41, 41, 40, 8, 39, 129, 1851179216, 210585684, 2864837593, 1224628296, 3878985923, 2238658667, 597922252, 3252422748, 1611825377, 4266593207, 2625935261, 985020645, 3639189598, 1998936572, 358543424, 3012577522, 1372569855, 4026790860, 2814092282, 1173633122, 3828003064, 2187401233, 13, 7, 23, 24, 27, 22, 18, 11, 36, 19, 1, 56, 938, 1077, 1093, 4080163523, 56, 893, 894, 903, 956785024, 56, 982, 976, 983, 3620676529, 27, 24, 11, 18, 16, 12, 2, 21, 20, 15, 24, 22, 14, 17, 2, 24, 15, 13, 1, 12, 16, 2, 2, 12, 26, 21, 14, 21, 23, 2, 1, 6, 5, 20, 20, 14, 16, 21, 8, 32, 2, 15, 27, 22, 13, 15, 17, 20, 15, 8, 19, 24, 11, 11, 24, 19, 19, 20, 19, 8, 17, 12, 32, 3, 7, 34, 18, 20, 24, 15, 15, 13, 14, 15, 3, 0, 19, 23, 9, 2, 16, 9, 24, 9, 22, 0, 4, 8, 16, 7, 3, 10, 12, 421547801, 3075622252, 1435358687, 4089539957, 2449138526, 808740632, 3463123753, 1822932750, 182191363, 2836825401, 1195998477, 3850759454, 2210277136, 569424880, 3224317816, 1583502582, 4238329196, 2597014812, 956751137, 3610785574, 1970643752, 330112810, 2793296369, 1152450161, 3807199960, 2166736475, 525972420, 3180670530, 1540022998, 4193810884, 2553540171, 913251035, 3567289933, 1927154673, 3554869736, 1914603711, 273944715, 2928246671, 1287775156, 3942492317, 2302029977, 661178780, 3316072839, 1675360427, 34623334, 2688736137, 1048486026, 3702532279, 2062389643, 421857717, 3076030649, 1435900078, 4090213287, 2449946469, 809148669, 3463768421, 1823044039, 182573383, 2837201148, 1196604281, 3850426264, 2210097339, 569893022, 3223874018, 1583812949, 4237998273, 2597323595, 957188079, 3611652936, 1970858216, 330441077, 2985277944, 1344483614, 3999161106, 2358560544, 717835020, 3371815115, 1731471717, 5, 20, 1, 56, 892, 938, 941, 2486080494, 17, 6, 6, 12, 12, 2, 12, 18, 25, 6, 0, 0, 11, 4, 11, 18, 25, 5, 8, 7, 56, 897, 981, 983, 2861048524, 56, 1079, 920, 928, 1169694486, 56, 975, 1110, 1125, 318887172, 56, 984, 966, 968, 3443331114, 32, 16, 14, 56, 894, 906, 919, 302570143, 4, 2, 22, 1, 16, 17, 262479847, 2916725339, 1276524270, 3930882664, 2290552293, 649852494, 3304332016, 1663741517, 23149466, 2677823129, 1037159078, 3691085298, 2050803278, 4170390581, 2529748009, 889349126, 3544046692, 1903244514, 262816873, 2917457402, 1276857451, 3931632884, 2290902108, 650030131, 3304141887, 1663931459, 23465409, 2677636187, 1037374666, 3691680841, 2051030979, 410756168, 3065468104, 1424555093]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_2_4_1_3_7_5_0_6 (order: [2,4,1,3,7,5,0,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[4]]; + // DIV + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a / regs[base + _operands[2]]; + // NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a !== regs[base + _operands[5]]; + // MOVE + var dst = _operands[6]; + regs[base + dst] = regs[base + _operands[7]]; + break; + } + case 62: + { + // ANTI_NEQ_4_8_2_11_1_3_9_6_0_5_7_10_12 (order: [4,8,2,11,1,3,9,6,0,5,7,10,12]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[11], _unsortedOperands[3], _unsortedOperands[12]]; + // LOAD_GLOBAL + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // SUB + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a - regs[base + _operands[5]]; + // NEQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a !== regs[base + _operands[8]]; + // JUMP_IF_FALSE + var src = _operands[9]; + var target = _operands[10]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // LOAD_INT + var dst = _operands[11]; + regs[base + dst] = _operands[12]; + break; + } + case 63: + { + // ANTI_EQ_0_9_3_6_1_8_7_10_5_2_4_11 (order: [0,9,3,6,1,8,7,10,5,2,4,11]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[11]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // SHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >> regs[base + _operands[5]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[6]; + var count = _operands[7]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[8]]; + regs[base + dst] = elems; + // ADD + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a + regs[base + _operands[11]]; + break; + } + case 64: + { + // ANTI_SUB_1_4_0_7_6_5_2_8_3 (order: [1,4,0,7,6,5,2,8,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[7]]; + // LOAD_GLOBAL + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // SUB + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a - regs[base + _operands[8]]; + break; + } + case 65: + { + // ANTI_ADD_6_3_1_11_7_10_4_0_5_9_8_2 (order: [6,3,1,11,7,10,4,0,5,9,8,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[11], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[10], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[3]]; + // TYPEOF + var dst = _operands[0]; + regs[base + dst] = typeof regs[base + _operands[1]]; + // ADD + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a + regs[base + _operands[4]]; + // LOAD_INT + var dst = _operands[5]; + regs[base + dst] = _operands[6]; + // UNARY_NOT + var dst = _operands[7]; + regs[base + dst] = !regs[base + _operands[8]]; + // ADD + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a + regs[base + _operands[11]]; + break; + } + case 66: + { + // ANTI_LT_10_12_11_0_14_6_13_1_4_5_9_2_3_7_8 (order: [10,12,11,0,14,6,13,1,4,5,9,2,3,7,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[13], _unsortedOperands[14], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[4]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this._constant(_operands[1], _operands[2]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // MUL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a * regs[base + _operands[5]]; + // LTE + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a <= regs[base + _operands[8]]; + // LT + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a < regs[base + _operands[11]]; + // NEQ + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a !== regs[base + _operands[14]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/dispatcher-anti-2.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/dispatcher-anti-2.encoded.js new file mode 100644 index 00000000..9abdda42 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/dispatcher-anti-2.encoded.js @@ -0,0 +1,1336 @@ +var CONSTANTS = [/* 0 */1, /* 1 */undefined, /* 2 */"c1uyvl", /* 3 */"0679DSPH", /* 4 */56007, /* 5 */6968, /* 6 */0, /* 7 */"atob", /* 8 */"", /* 9 */"length", /* 10 */33127, /* 11 */"String", /* 12 */"rj7hSXKM", /* 13 */"fromCharCode", /* 14 */"charCodeAt", /* 15 */2, /* 16 */"XfeqtXYx", /* 17 */"BSe8rDHd", /* 18 */"HffGH31K", /* 19 */30065, /* 20 */20810, /* 21 */"Infinity", /* 22 */"K0VuvRyoYu", /* 23 */2654435769, /* 24 */13, /* 25 */"RegExp", /* 26 */65535, /* 27 */"NaN", /* 28 */"isNaN", /* 29 */"4ILWIZMw", /* 30 */41286, /* 31 */26705, /* 32 */16, /* 33 */19, /* 34 */"Date", /* 35 */"Math", /* 36 */"imul", /* 37 */4166566615, /* 38 */3397930814, /* 39 */2589111490, /* 40 */"parseFloat", /* 41 */"XWY9OtR", /* 42 */"kirN", /* 43 */3341819845, /* 44 */3784729070, /* 45 */"Function", /* 46 */6346, /* 47 */11726, /* 48 */"chr", /* 49 */2980958453, /* 50 */1992822377, /* 51 */18, /* 52 */"parseInt", /* 53 */14599, /* 54 */52021, /* 55 */"MMH3ANjc", /* 56 */"Boolean", /* 57 */"VdDrWa", /* 58 */8, /* 59 */"r6hc", /* 60 */33344, /* 61 */"2sWca", /* 62 */"M1MJSbeJEuB+oct/dFsWyDEdLsmYM5pswtXRlBkkF6zfXbv4CQpK6usPxiAiMdEBqvC7c3iiCP2YO8x3Jll5DW6FaRjNDtX1Fs4pAGn7kBLt+AOH2zGKrS0NKcdd6b7TF3Ln8jYSz7bj+sDWPqRAhnsISdm91kiMAFAP56jta3JpWbkEy/M3UDFwmMwM3mXri24ENW6lyZ+UtiLwhDRiR2VYiYkIB8BJp9TOKufG/WRMay1lANjEeVD9i0LMFUO1RgX3uy3IwaVhSaNTyKaGGxowgLu4+WahMB0MlBHyXQcBlDmEwLe72Nps1IJtEdncAXINOhuV/nXNkV6FrE0TNwzol5kKhSw03SdNQGjSbTBC+ko6GNGLUpFQZL1J6U1fxmcv0s7kZ1bBbfrmOezUiYzV4iS7XwdPniOUT+nmzkIxSHThZR5gNTAP1KybromW/skPX7vboQ6WrMCvZtsPKhONGS/XZa5e+eGilpi/1gQ/rUQXoeT+AiJCsRpK7NoayKbSiAeAywZsXOWCQ+rDJNSpj5qALL4LCX5s1LCIRaO6pM50kB8LdWgpOKqVPwCLiRAbeavhPlmEMAjPh0P/+OlwN9maOZQm7lxp74lg7G5Rgm6tSGyhEC2Sn67SWJx6hZvEx+4NQPR74TAmAF46Z1gEY3/B+MA3azK+BkICz46idpp/ZcTIOhuGrcxVxTsaNZ6GpTUTxHsKU8DuoFcUKMbm5rtlawC7zUBlEvW+SYk9zx7cgRgRyOfB/ntLX0dBfYxBq/VP7zVvHSx2BVtSqjsCwhaESRblas7M+NEfixFXIl/XakFtUuac+B8Mke9zt348YfGS2FLzxgvRP3M2E5wYxLVqT5VnNTsO0AcRCdKXZGTQ8U5k1yY/aDL8eXqfK1oliDTS6XV1k7GfzSdq7Ska75V76O2pUWS8ui/hY2QJqb1W/0eHD9urD+EUISAY/jcPuayB5Y2DD5R3oVbFMb1/ian/QRT/f6q26ySovrytYk42fyl0L1LJ0mcRxFv8kt/HraWDiP49XiUyLZDHTeP4wBI9tBzOFQ/VfBlw7gca897DP77eqd7qfBQdRi1qdYgCdsRdpPKMFHPrcS5DeAkAdWJPB/GkeBqywA2us4oEsw5JPxRwR5NjYS2Phg==", /* 63 */"window", /* 64 */4]; +var BYTECODE = [56, 5, 2346, 2440, 3578070050, 26, 13, 2, 14, 56, 2211, 2274, 2285, 486972988, 56, 2114, 2094, 2101, 1050535803, 14, 17, 16, 22, 25, 14, 14, 5, 13, 20, 21, 6, 21, 19, 23, 18, 56, 2220, 2162, 2173, 366820770, 12, 13, 23, 22, 35, 5, 5, 56, 2305, 2251, 2252, 2032389022, 21, 18, 23, 17, 56, 2312, 2154, 2159, 1223135701, 55, 1, 10, 40081, 26, 13, 1, 20, 56, 2096, 2146, 2154, 950981058, 11, 3, 21, 11, 35, 22, 21, 56, 2248, 2124, 2138, 4281866843, 56, 2281, 2114, 2122, 2901963837, 16, 11, 1, 12, 21, 7, 18, 19, 45, 14, 56, 2309, 2210, 2218, 2287456720, 56, 111, 1930, 1951, 1862936327, 14, 16, 17, 12, 12, 16, 5, 0, 18, 18, 0, 14, 56, 2154, 2219, 2223, 3274398783, 34, 2, 14, 16, 56, 137, 2079, 2091, 3115506933, 24, 9, 17, 9, 56, 2156, 2106, 2109, 3289987163, 55, 4, 5, 56, 154, 2330, 2346, 3329730869, 56, 2117, 2247, 2262, 3980958528, 21, 21, 11, 16, 56, 2152, 2262, 2264, 2193332574, 55, 5, 56, 175, 1974, 2063, 442161232, 21, 3, 8, 19, 20, 11, 5, 24, 55, 16, 1, 3, 18, 14, 1, 15, 10, 56, 2238, 2306, 2307, 3205272678, 4, 9, 18, 12, 22, 4, 24, 11, 11, 12, 4, 12, 22, 14, 24, 15, 56, 2235, 2317, 2330, 836179430, 25, 6, 20, 12, 13, 8, 16, 5, 56, 2118, 2112, 2116, 74863097, 20, 14, 19, 13, 56, 2116, 2111, 2112, 374203134, 14, 17, 8, 15, 16, 13, 1, 3, 56, 2249, 2228, 2235, 64716417, 56, 2116, 2242, 2245, 116132022, 56, 2253, 2237, 2248, 3446500734, 36, 39, 368, 56, 271, 1960, 1974, 1770495532, 16, 19, 21, 15, 21, 22, 20, 18, 56, 2112, 2210, 2218, 2858180128, 28, 56, 290, 2067, 2079, 1594084488, 16, 19, 18, 14, 22, 11, 6, 8, 1, 19, 49, 51, 61, 12, 12, 14, 12, 13, 13, 12, 12, 4, 12, 12, 0, 14, 0, 0, 67, 2, 13, 12, 13, 15, 13, 13, 13, 13, 12, 14, 4, 13, 61, 13, 13, 4, 12, 13, 13, 13, 13, 15, 13, 13, 56, 349, 1951, 1960, 1515249364, 36, 2, 14, 12, 0, 22, 22, 55, 1, 39, 368, 45, 3, 0, 14, 1, 0, 45, 14, 42, 5, 8, 3, 5, 6, 7, 0, 9, 2, 0, 8, 5, 5, 9, 56, 388, 2264, 2266, 1327394233, 55, 38, 0, 11, 3, 0, 0, 13, 4, 0, 0, 12, 5, 0, 0, 14, 6, 0, 47, 8, 1521, 2, 10, 0, 0, 4, 2, 2, 15, 7, 0, 42, 16, 15, 1, 0, 61, 12, 12, 3, 12, 12, 12, 12, 11, 16, 12, 11, 0, 15, 8, 0, 61, 12, 12, 4, 12, 11, 11, 12, 12, 15, 12, 11, 0, 15, 6, 0, 61, 11, 12, 5, 12, 12, 11, 11, 11, 15, 12, 12, 0, 15, 9, 0, 8, 16, 3, 15, 62, 5, 12, 11, 16, 12, 15, 12, 12, 11, 12, 12, 63, 15, 9, 12, 11, 0, 0, 12, 11, 11, 10, 12, 11, 11, 11, 64, 11, 11, 12, 12, 9, 12, 12, 9, 11, 12, 12, 1, 6, 566458320, 1, 10, 284819161, 65, 10, 11, 11, 6, 12, 12, 11, 11, 11, 10, 11, 12, 12, 66, 11, 12, 0, 10, 10, 12, 12, 11, 9, 67, 1, 12, 11, 11, 6, 12, 11, 11, 11, 11, 10, 6, 11, 1, 7, 746259781, 39, 737, 2, 15, 11, 0, 0, 16, 13, 0, 8, 17, 15, 16, 0, 16, 14, 0, 8, 18, 3, 16, 43, 16, 3, 18, 1, 5, 0, 18, 14, 0, 8, 19, 3, 18, 0, 18, 0, 0, 67, 2, 11, 12, 12, 20, 12, 12, 11, 12, 12, 18, 5, 12, 43, 18, 3, 19, 1, 20, 56, 627, 2063, 2067, 1733261536, 18, 4, 6, 16, 68, 12, 12, 11, 18, 643, 20, 13, 19, 12, 12, 12, 69, 12, 20, 12, 11, 16, 654, 18, 12, 12, 14, 43, 16, 15, 17, 1, 18, 67, 0, 11, 12, 12, 15, 12, 11, 12, 11, 11, 16, 4, 12, 61, 12, 12, 4, 11, 12, 11, 11, 11, 15, 11, 11, 0, 15, 15, 0, 67, 2, 11, 12, 12, 16, 12, 12, 11, 12, 12, 15, 5, 12, 61, 12, 11, 5, 12, 12, 11, 12, 12, 16, 11, 12, 1, 6, 994921552, 1, 7, 2620332591, 39, 737, 56, 2208, 2092, 2099, 1023935391, 45, 4, 0, 15, 1, 0, 45, 15, 42, 6, 8, 2, 7, 6, 58, 6, 0, 16, 16, 0, 0, 14, 17, 0, 0, 15, 18, 0, 0, 18, 6, 0, 0, 17, 19, 0, 47, 11, 1722, 2, 11, 0, 0, 4, 5, 0, 19, 8, 0, 61, 16, 16, 6, 16, 16, 16, 16, 15, 19, 15, 14, 56, 795, 2266, 2270, 3149966988, 24, 23, 17, 0, 61, 16, 15, 7, 15, 15, 15, 15, 14, 19, 15, 16, 62, 7, 15, 15, 3, 15, 19, 16, 16, 14, 15, 15, 63, 19, 12, 16, 15, 0, 0, 16, 15, 15, 20, 14, 21, 14, 15, 64, 15, 15, 15, 16, 12, 15, 16, 12, 15, 15, 15, 1, 9, 268671787, 1, 13, 3725481444, 65, 13, 14, 14, 9, 15, 16, 16, 15, 15, 13, 15, 14, 16, 66, 14, 16, 0, 13, 13, 16, 22, 14, 12, 67, 0, 14, 16, 14, 9, 16, 16, 16, 14, 14, 13, 9, 16, 1, 10, 2925355475, 39, 1158, 32, 14, 8, 56, 2111, 2227, 2231, 1240361835, 0, 19, 23, 0, 67, 1, 14, 15, 16, 20, 15, 14, 14, 15, 14, 19, 1, 14, 0, 19, 6, 0, 69, 16, 19, 15, 16, 20, 940, 21, 15, 14, 18, 61, 15, 16, 1, 15, 15, 16, 14, 14, 21, 15, 15, 0, 19, 24, 0, 70, 14, 15, 14, 14, 1, 19, 20, 71, 15, 15, 1, 14, 15, 15, 25, 15, 20, 19, 14, 0, 15, 15, 14, 0, 20, 26, 0, 72, 16, 15, 16, 0, 20, 21, 27, 16, 15, 19, 16, 61, 15, 16, 8, 14, 14, 14, 14, 16, 21, 16, 14, 2, 19, 11, 0, 0, 20, 13, 0, 8, 21, 19, 20, 0, 20, 14, 0, 8, 22, 0, 20, 67, 1, 16, 16, 16, 20, 14, 16, 16, 14, 14, 7, 2, 14, 43, 23, 0, 22, 1, 20, 71, 16, 14, 23, 15, 15, 15, 28, 14, 8, 20, 14, 0, 16, 15, 14, 43, 22, 19, 21, 1, 20, 67, 2, 14, 14, 15, 19, 16, 15, 14, 14, 16, 22, 6, 16, 61, 15, 14, 6, 16, 16, 14, 15, 16, 19, 15, 14, 61, 16, 14, 19, 16, 16, 16, 16, 16, 7, 14, 16, 0, 19, 0, 0, 67, 1, 15, 16, 16, 20, 14, 16, 16, 16, 14, 19, 7, 16, 61, 14, 16, 7, 15, 14, 15, 16, 14, 20, 14, 14, 1, 9, 405625643, 1, 10, 574759347, 39, 1158, 11, 22, 1, 14, 45, 6, 0, 19, 1, 0, 45, 19, 42, 9, 11, 2, 9, 10, 58, 9, 0, 7, 29, 0, 0, 6, 30, 0, 0, 9, 6, 0, 0, 8, 31, 0, 4, 4, 0, 10, 32, 0, 68, 6, 7, 6, 2, 1200, 11, 8, 10, 6, 6, 6, 69, 7, 11, 7, 6, 1, 1211, 10, 7, 6, 9, 0, 11, 6, 0, 70, 6, 7, 7, 6, 10, 11, 12, 61, 6, 6, 5, 6, 7, 7, 6, 6, 12, 7, 7, 73, 1248, 9, 7, 6, 6, 0, 6, 6, 10, 6, 6, 6, 0, 11, 6, 0, 70, 6, 6, 6, 6, 10, 11, 12, 61, 6, 6, 0, 7, 7, 7, 7, 7, 12, 6, 6, 0, 10, 33, 0, 70, 6, 7, 7, 7, 0, 10, 11, 71, 6, 6, 0, 6, 6, 7, 34, 7, 11, 10, 7, 0, 7, 6, 6, 0, 11, 6, 0, 70, 7, 6, 7, 7, 10, 11, 12, 61, 7, 6, 0, 6, 6, 6, 6, 6, 12, 6, 6, 2, 10, 35, 0, 0, 11, 36, 0, 8, 12, 10, 11, 0, 11, 37, 0, 43, 13, 10, 12, 2, 0, 11, 0, 10, 6, 0, 70, 7, 6, 6, 6, 13, 10, 11, 61, 7, 7, 0, 7, 7, 7, 6, 6, 11, 7, 6, 0, 10, 38, 0, 65, 0, 7, 7, 10, 7, 6, 6, 7, 6, 11, 7, 6, 7, 0, 10, 6, 0, 70, 7, 7, 7, 7, 11, 10, 12, 61, 7, 7, 0, 7, 6, 6, 7, 7, 12, 7, 7, 67, 1, 6, 7, 6, 10, 6, 7, 7, 7, 7, 5, 0, 7, 0, 11, 6, 0, 70, 6, 6, 7, 7, 10, 11, 12, 61, 7, 6, 0, 7, 7, 6, 6, 7, 12, 7, 6, 0, 10, 39, 0, 71, 6, 6, 0, 7, 7, 6, 40, 7, 10, 11, 6, 0, 7, 7, 7, 0, 10, 6, 0, 70, 6, 7, 7, 7, 11, 10, 12, 61, 7, 7, 0, 6, 7, 7, 6, 7, 12, 6, 6, 0, 10, 2, 0, 49, 11, 1, 10, 0, 45, 11, 56, 2201, 2239, 2243, 3038128895, 24, 11, 14, 1, 0, 10, 1, 0, 45, 10, 0, 4, 41, 0, 0, 5, 42, 0, 4, 3, 2, 6, 35, 0, 0, 7, 36, 0, 8, 8, 6, 7, 0, 7, 43, 0, 43, 9, 6, 8, 2, 1, 7, 0, 6, 6, 0, 70, 5, 4, 4, 4, 9, 6, 7, 61, 5, 5, 1, 5, 4, 4, 5, 4, 7, 4, 4, 0, 6, 44, 0, 71, 5, 5, 1, 5, 4, 5, 45, 4, 6, 7, 4, 0, 4, 5, 5, 0, 6, 6, 0, 70, 5, 5, 4, 5, 7, 6, 8, 61, 5, 4, 1, 4, 5, 5, 4, 5, 8, 4, 5, 0, 6, 33, 0, 70, 5, 4, 5, 5, 1, 6, 7, 71, 5, 5, 1, 4, 4, 5, 25, 4, 7, 6, 4, 0, 5, 5, 4, 0, 7, 6, 0, 70, 5, 4, 4, 5, 6, 7, 8, 61, 4, 4, 1, 4, 4, 4, 4, 4, 8, 4, 5, 71, 4, 4, 1, 4, 4, 4, 11, 4, 0, 6, 4, 0, 4, 5, 4, 0, 7, 6, 0, 70, 4, 4, 4, 4, 6, 7, 8, 61, 4, 4, 1, 4, 4, 4, 4, 4, 8, 4, 4, 45, 1, 0, 6, 1, 0, 45, 6, 0, 4, 46, 0, 0, 6, 47, 0, 0, 5, 48, 0, 4, 3, 2, 7, 35, 0, 0, 8, 36, 0, 8, 9, 7, 8, 0, 8, 49, 0, 43, 10, 7, 9, 2, 0, 8, 0, 7, 6, 0, 70, 5, 4, 4, 4, 10, 7, 8, 61, 6, 4, 0, 6, 5, 5, 4, 4, 8, 4, 5, 71, 4, 5, 0, 5, 4, 4, 45, 5, 1, 7, 6, 0, 6, 6, 5, 0, 8, 6, 0, 70, 5, 6, 6, 5, 7, 8, 9, 61, 6, 6, 0, 5, 4, 5, 6, 4, 9, 6, 5, 0, 7, 50, 0, 65, 0, 4, 4, 7, 5, 5, 5, 4, 5, 8, 5, 6, 5, 0, 7, 6, 0, 70, 6, 4, 6, 4, 8, 7, 9, 61, 5, 4, 0, 6, 4, 5, 4, 6, 9, 4, 4, 0, 7, 51, 0, 70, 6, 5, 5, 6, 0, 7, 8, 71, 4, 6, 0, 6, 4, 4, 52, 4, 8, 7, 4, 0, 5, 5, 4, 0, 8, 6, 0, 70, 4, 4, 4, 5, 7, 8, 9, 61, 6, 4, 0, 6, 6, 4, 6, 4, 9, 4, 4, 45, 0, 18, 16, 0, 1, 55, 0, 7, 1, 0, 45, 7, 222415660, 2877047070, 1236457762, 3891212556, 2249963769, 609624843, 3263721266, 1623651103, 4277836077, 2637168909, 997047734, 3651473673, 2010704665, 370310928, 3025136493, 1384339186, 4039031162, 2398405103, 757684588, 3411669799, 1771335468, 4170161409, 2528864000, 1917220934, 3542638362, 1902496530, 261992646, 2916113712, 1276038451, 3930295351, 129967463, 2784214305, 1143745842, 3798111526, 2157509418, 517246932, 3171818336, 1531033340, 4185730430, 2545343939, 904426837, 3559387841, 1918590797, 277922766, 3096942100, 1456175675, 4110872109, 2470192683, 829534749, 3483487844, 1843151865, 202819198, 2857071357, 1216863769, 3870959126, 2230364675, 590159419, 3244657167, 1603798539, 4258634257, 2618101259, 977320458, 3632145021, 1991503612, 350712350, 3004880402, 1364476447, 4018748968, 2378289669, 737962540, 3392339499, 1752132122, 111384549, 2766027385, 1125158654, 3779926594, 2139256286, 498724436, 3153437381, 1512384054, 4166562379, 2526174927, 886030929, 3540207048, 1899945551, 3725592847, 2913543793, 1273076702, 3570685774, 2287401705, 646645164, 3301412270, 1660564140, 19961504, 2674713494, 1033797218, 3687961336, 2047694460, 407190941, 3061369495, 1421235866, 4075551622, 2435280816, 794434242, 3449075342, 1808338322, 167878024, 2822506238, 1181938548, 3836682237, 2195432110, 555099798, 3209387426, 1568984551, 4223288992, 2582764195, 942493609, 3596794709, 1956128491, 315780465, 2970615291, 1329829215, 3984458448, 2343878469, 703140801, 3357120186, 1716813132, 76475844, 2730710360, 1090482058, 3744714614, 2104450037, 463762904, 92723323, 2746971109, 1106765867, 3760873500, 4248612484, 2608208518, 967571337, 3622243200, 1981456003, 340789896, 2995631604, 1354571394, 4008731295, 2368399262, 728191905, 3382431414, 1474881882, 4129717996, 2489183073, 848386021, 3503209300, 1862058275, 221731134, 2875978045, 1235513024, 3889865556, 2249271243, 609071936, 56, 2243, 2240, 2249, 1257731009, 27, 6, 7, 8, 22, 15, 14, 10, 56, 2246, 2249, 2251, 3865228885, 22, 23, 10, 0, 25, 23, 17, 2, 56, 2123, 2185, 2200, 4149953004, 24, 17, 4, 0, 56, 2274, 2326, 2328, 449819889, 22, 19, 5, 9, 1, 18, 33225, 35, 6, 24, 32, 16, 20, 12, 6, 10, 14, 22, 14, 0, 4, 32, 16, 10, 24, 8, 20, 23, 24, 14, 22, 24, 1, 24, 7166, 13, 0, 13, 24, 14, 1, 11, 8, 1, 18, 12450, 56, 2198, 2303, 2308, 2255998872, 56, 2241, 2216, 2232, 4273614779, 36, 5, 4, 1, 23, 34607, 14, 0, 8, 3, 56, 2147, 2235, 2240, 3738747944, 34, 2, 0, 25, 8, 17, 13, 35, 20, 1, 24, 10, 19, 17, 36, 11, 18, 25, 16, 19, 2, 25, 4, 1, 20, 1, 7, 43741, 21, 24, 18, 2, 24, 5, 11, 1, 26, 9, 1, 20, 55, 20, 2, 9, 2, 55, 56, 2120, 2119, 2127, 693572949, 13, 0, 18, 20, 32, 18, 13, 22, 21, 8, 12, 3981610851, 2341012416, 1509615761, 4163395687, 2523113980, 882847887, 56, 2219, 2097, 2100, 265128879, 5, 20, 12, 26, 0, 4, 10, 12, 12, 9, 4, 56, 2241, 2253, 2264, 3830637184, 26, 8, 21, 10, 8, 22, 5, 19, 56, 2301, 2201, 2203, 2996188761, 16, 2, 10, 7, 56, 2241, 2312, 2314, 3940834391, 36, 16, 0, 24, 1, 10, 23, 56, 2125, 2168, 2174, 3363719520, 56, 2219, 2205, 2209, 883567675, 1688995345, 48663067, 2702908972, 1062496831, 3716933145, 2076662317, 435892845, 3090593512, 1449673323, 4104507383, 2463986272, 823187165, 3478037086, 1837317569, 196580916, 2850687560, 1937761795, 296974859, 2951614002, 1310759452, 3965519383, 2325056255, 684203584, 3339119081, 1698390634, 57662996, 2711761438, 1071597094, 3725640210, 2085506605, 444985425, 3099155026, 1459007173, 4113316469, 2473039866, 832244303, 3486809794, 1846151800, 205558209, 2860334742, 1219338819, 3873462223, 2233135709, 592924375, 3247161949, 1606924792, 4261096577, 2620447458, 980051525, 3634752416, 1993940647, 353536702, 3008102825, 1367579066, 4022334108, 2381604581, 740746229, 3394809446, 1754608265, 114153884, 2768257466, 1128200847, 3782432433, 2141727627, 501464967, 3156159122, 1515230875, 4170086907, 2529526128, 888739558, 3543577450, 1902391209, 262103714, 2916349611, 1275883909, 3930249647, 2289649365, 649415518, 3243407694, 1663069053, 22734590, 2677447068, 1036530141, 3691493707, 2050687689, 410029845, 3064099659, 1423828690, 4077986137, 2437593556, 797233535, 3451603699, 1811414883, 170675173, 2825312045, 1184446831, 3839205668, 2198739219, 557871417, 3212644126, 1571980054, 4225775485, 2585506548, 945232135, 3599294729, 1959160079, 318627584, 2972797707, 1332627260, 3987004168]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 17; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_1_7_3_0_10_8_2_6_4_5_9 (order: [1,7,3,0,10,8,2,6,4,5,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[4]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // MOVE + var dst = _operands[3]; + regs[base + dst] = regs[base + _operands[4]]; + // LT + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a < regs[base + _operands[7]]; + // MOD + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a % regs[base + _operands[10]]; + break; + } + case 62: + { + // ANTI_LT_7_4_9_8_3_6_0_5_1_2_10 (order: [7,4,9,8,3,6,0,5,1,2,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[10]]; + // LTE + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a <= regs[base + _operands[2]]; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // LT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a < regs[base + _operands[8]]; + // TYPEOF + var dst = _operands[9]; + regs[base + dst] = typeof regs[base + _operands[10]]; + break; + } + case 63: + { + // ANTI_UNARY_NOT_4_3_11_6_13_2_7_10_5_12_9_1_8_0 (order: [4,3,11,6,13,2,7,10,5,12,9,1,8,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[13], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[12], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[4]]; + // LOAD_GLOBAL + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // UNARY_NOT + var dst = _operands[3]; + regs[base + dst] = !regs[base + _operands[4]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[7]]); + // GTE + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a >= regs[base + _operands[10]]; + // LOAD_CONST + var dst = _operands[11]; + regs[base + dst] = this._constant(_operands[12], _operands[13]); + break; + } + case 64: + { + // ANTI_UNARY_POS_3_9_7_8_0_10_4_1_6_5_2 (order: [3,9,7,8,0,10,4,1,6,5,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[5]]; + // UNARY_POS + var dst = _operands[0]; + regs[base + dst] = +regs[base + _operands[1]]; + // EQ + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a === regs[base + _operands[4]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[7]]); + // LTE + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a <= regs[base + _operands[10]]; + break; + } + case 65: + { + // ANTI_SUB_4_11_10_5_12_0_1_8_6_3_2_7_9 (order: [4,11,10,5,12,0,1,8,6,3,2,7,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[9], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[11], _unsortedOperands[7], _unsortedOperands[12], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[4]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // SUB + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a - regs[base + _operands[5]]; + // UNARY_NOT + var dst = _operands[6]; + regs[base + dst] = !regs[base + _operands[7]]; + // UNARY_NOT + var dst = _operands[8]; + regs[base + dst] = !regs[base + _operands[9]]; + // LT + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a < regs[base + _operands[12]]; + break; + } + case 66: + { + // ANTI_MUL_3_5_2_6_7_4_1_0_8 (order: [3,5,2,6,7,4,1,0,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[8]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this._constant(_operands[1], _operands[2]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + break; + } + case 67: + { + // ANTI_ADD_10_9_4_8_0_5_12_11_3_7_2_1_6 (order: [10,9,4,8,0,5,12,11,3,7,2,1,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[12], _unsortedOperands[9], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[6]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // DIV + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a / regs[base + _operands[5]]; + // EQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a === regs[base + _operands[8]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[9]; + var pairCount = _operands[10]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[11]]; + var val = regs[base + _operands[12]]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + case 68: + { + // ANTI_SHL_0_3_5_9_7_8_6_10_2_1_4 (order: [0,3,5,9,7,8,6,10,2,1,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[7]]; + // LOOSE_EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a == regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // JUMP_IF_FALSE + var src = _operands[6]; + var target = _operands[7]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // SHL + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a << regs[base + _operands[10]]; + break; + } + case 69: + { + // ANTI_BOR_2_6_3_8_5_1_4_9_7_0 (order: [2,6,3,8,5,1,4,9,7,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[7]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[0]; + var target = _operands[1]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // UNARY_NEG + var dst = _operands[2]; + regs[base + dst] = -regs[base + _operands[3]]; + // BOR + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a | regs[base + _operands[6]]; + // MOD + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a % regs[base + _operands[9]]; + break; + } + case 70: + { + // ANTI_USHR_2_0_1_3_5_6_4 (order: [2,0,1,3,5,6,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[5]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // UNARY_NEG + var dst = _operands[2]; + regs[base + dst] = -regs[base + _operands[3]]; + // USHR + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a >>> regs[base + _operands[6]]; + break; + } + case 71: + { + // ANTI_BXOR_11_8_1_6_5_4_13_9_2_0_12_14_7_3_10 (order: [11,8,1,6,5,4,13,9,2,0,12,14,7,3,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[13], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[12], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[14], _unsortedOperands[0], _unsortedOperands[10], _unsortedOperands[6], _unsortedOperands[11]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // BXOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a ^ regs[base + _operands[8]]; + // SUB + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a - regs[base + _operands[11]]; + // LOAD_GLOBAL + var dst = _operands[12]; + var globalName = this._constant(_operands[13], _operands[14]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 72: + { + // ANTI_BAND_5_7_3_2_10_8_1_6_4_9_0 (order: [5,7,3,2,10,8,1,6,4,9,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[4]]; + // LOAD_GLOBAL + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // UNARY_POS + var dst = _operands[3]; + regs[base + dst] = +regs[base + _operands[4]]; + // MUL + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a * regs[base + _operands[7]]; + // BAND + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a & regs[base + _operands[10]]; + break; + } + case 73: + { + // ANTI_UNARY_BITNOT_6_5_8_3_4_1_10_7_0_9_2_11 (order: [6,5,8,3,4,1,10,7,0,9,2,11]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[11]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // BXOR + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a ^ regs[base + _operands[4]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[5]; + var target = _operands[6]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // UNARY_BITNOT + var dst = _operands[7]; + regs[base + dst] = ~regs[base + _operands[8]]; + // MOD + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a % regs[base + _operands[11]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/dispatcher-anti-7.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/dispatcher-anti-7.encoded.js new file mode 100644 index 00000000..be4c1665 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/dispatcher-anti-7.encoded.js @@ -0,0 +1,1343 @@ +var CONSTANTS = [/* 0 */undefined, /* 1 */61243, /* 2 */"8tpKF", /* 3 */"atob", /* 4 */"", /* 5 */0, /* 6 */"length", /* 7 */"String", /* 8 */"fromCharCode", /* 9 */"charCodeAt", /* 10 */1, /* 11 */8, /* 12 */2, /* 13 */56934, /* 14 */9527, /* 15 */"aCnFdjZ", /* 16 */2654435769, /* 17 */13, /* 18 */65535, /* 19 */"p6rts", /* 20 */"yePkiEI", /* 21 */"vacC71DF", /* 22 */"Math", /* 23 */"imul", /* 24 */3039547097, /* 25 */10, /* 26 */22, /* 27 */29, /* 28 */2834158692, /* 29 */1038224232, /* 30 */"n4vdo", /* 31 */"fZjhn", /* 32 */"W2PA0C", /* 33 */16, /* 34 */3275536835, /* 35 */1798818339, /* 36 */31, /* 37 */5360, /* 38 */36710, /* 39 */35463, /* 40 */2855765791, /* 41 */2348501242, /* 42 */20, /* 43 */2725244355, /* 44 */2367010027, /* 45 */31106, /* 46 */"3A8W+vOyfYXQZ1t3az2mjdW6AELsJ5vDtoR3MjJeEkvkiNxLWvpeP7bg1zDGJJYne0pDjArDExO26b+NIp8arkv315/U69/TPArILGNb4WwqBY4WnMR0Xj1t+nc78gq0NLClVDhbhyMfIrzpilK8MNihNlYrCyuv3+tBjJrTvqd96/YdqYcvphzk4/WE1h+YwPau926MitF1AR2E+1NpuauMYsEY+ruOutIUbnGkEb07jfhPoTxiAikGgvFX1W+hrwNvnkuYg7ev/nz8i99nYnbmbJJ0CliGRRpZjiZ6ruZbKtifg9SZI4P+xoEfkS8pa2XuNUTQj9xlvJCogP37m4i8lPQ9fdI6gZWxy5ranjANjkp6kIYhLilAX3A+erwr0gBTLCsGcxtV4Jn7ZA+pLLBnvn96u0VeKVyhyvK11Myth17Mg2kIkOpK+obHxnsUkECr087TUMYJbl5bmutXQIVcGnqO1G9nffkgjbJvq1Z7iRJLWuJ/km38TNfiftJvmrJa7veMSaiSKXyxC2QWnCTbQWKeSywX965su/H2QlF3QkERabga7cDIK9R29vw5j/pXSNaJHmSrtpbCrJ+7uRqfHJicRfrUi3vLI/i4wXY9jyKNVeP58z4nDBpzSRyGi2T56HXbtYdw6FpHPX4K7QmtuctiVdnoOPjB168A4CqrwMIaIBVt6XFF3JYdQz3LNri3DlhywPoWFeAVlmEKyvmwnkVefzkbkXvpb1WLAiuzxZ5NHLEc1XUJmi6Zt5fkKUgA1/SkilJDGFMbB/2EsfxBl4QfEhaELm15x4opvL2jKyY9YjRh4eIHjwi18Mvr1ntdk3+0sSughBETY5izuF7Y2xH2kdJeSA6h69ZFvCeUSPpYFcvY0rTdCMY=", /* 47 */"window", /* 48 */4, /* 49 */7458, /* 50 */"e5ZR2BW4", /* 51 */"izdbW", /* 52 */41528]; +var BYTECODE = [56, 5, 2408, 2412, 863561099, 16, 11, 2, 11, 56, 14, 2270, 2354, 81850140, 17, 14, 5, 14, 34, 21, 16, 1, 20, 6105, 11, 13, 13, 10, 19, 22, 5, 14, 18, 3, 12, 14, 1, 7, 56191, 12, 16, 22, 4, 25, 9, 20, 15, 18, 8, 11, 17, 56, 2662, 2208, 2213, 2988359325, 55, 14, 5, 0, 0, 14, 16, 13, 5, 56, 2529, 2516, 2524, 2315475582, 13, 1, 22, 21, 20, 11, 10, 7, 11, 19, 21, 23, 35, 19, 9, 56, 2610, 2670, 2682, 98088079, 32, 19, 22, 11, 19, 19, 15, 20, 45, 13, 34, 5, 12, 22, 8, 3, 5, 56, 112, 2489, 2493, 1593836360, 32, 8, 14, 14, 56, 121, 2552, 2587, 2006825918, 18, 4, 4, 8, 56, 2600, 2664, 2679, 695724491, 56, 2660, 2599, 2613, 3718846486, 56, 2627, 2512, 2523, 1173900264, 21, 20, 19, 6, 22, 20, 7, 18, 5, 22, 14, 56, 2220, 2230, 2235, 4073607877, 56, 161, 2542, 2552, 1381775836, 8, 4, 0, 6, 12, 17, 9, 21, 4, 22, 56, 176, 2418, 2489, 1019003089, 19, 8, 15, 19, 16, 14, 3, 8, 14, 21, 10, 2, 8, 22, 14, 24, 56, 2522, 2667, 2675, 3216038080, 21, 5, 17, 14, 19, 0, 20, 23, 56, 2249, 2232, 2243, 2510078751, 35, 20, 20, 13, 3, 10, 3, 56, 2250, 2668, 2679, 3845156357, 56, 2672, 2259, 2268, 831344939, 56, 2599, 2605, 2615, 1570213214, 12, 12, 7, 0, 23, 11, 20, 19, 13, 9, 18, 6, 35, 18, 18, 39, 351, 56, 254, 2364, 2378, 1238148833, 20, 5, 8, 11, 19, 19, 17, 11, 13, 13, 6, 4, 55, 25, 56, 273, 2648, 2658, 3798167789, 34, 5, 4, 26, 20, 10, 11, 55, 55, 25, 56, 288, 2493, 2507, 1042897174, 25, 23, 1, 8, 25, 23, 11, 9, 56, 2208, 2602, 2613, 3076560525, 54, 56, 307, 2378, 2408, 2739205049, 21, 21, 20, 11, 34, 1, 16, 27, 5, 3, 14, 20, 21, 2, 1, 56, 2259, 2663, 2665, 1508005257, 26, 18, 8, 11, 27, 14, 0, 0, 4, 14, 39, 351, 45, 3, 0, 14, 0, 0, 45, 14, 16, 19, 13, 13, 56, 356, 2412, 2418, 1678604833, 13, 9, 24, 7, 55, 20, 56, 367, 2354, 2364, 740533028, 56, 2663, 2516, 2523, 1533415872, 4, 0, 55, 4, 10, 0, 13, 1, 0, 0, 14, 2, 0, 47, 9, 1449, 3, 12, 0, 0, 4, 2, 2, 15, 3, 0, 42, 16, 15, 1, 0, 56, 408, 2683, 2693, 2793174794, 27, 9, 11, 17, 5, 13, 14, 4, 12, 8, 0, 15, 4, 0, 61, 13, 4, 14, 15, 1, 13, 14, 13, 13, 0, 15, 5, 0, 61, 13, 5, 13, 15, 0, 13, 13, 13, 14, 0, 15, 6, 0, 8, 16, 3, 15, 62, 5, 14, 13, 13, 13, 13, 14, 14, 15, 13, 16, 14, 63, 15, 13, 13, 11, 13, 14, 14, 64, 13, 14, 14, 11, 14, 11, 14, 68, 11, 13, 11, 13, 14, 14, 13, 14, 13, 1, 6, 2616759522, 1, 12, 105798654, 65, 14, 12, 13, 14, 6, 14, 14, 12, 14, 69, 13, 14, 13, 14, 14, 12, 11, 14, 14, 12, 14, 14, 14, 14, 14, 67, 6, 14, 13, 14, 13, 13, 14, 13, 12, 6, 14, 13, 13, 1, 7, 23725, 1, 8, 49971, 39, 729, 2, 15, 7, 0, 0, 16, 8, 0, 8, 17, 15, 16, 0, 16, 9, 0, 8, 18, 3, 16, 43, 16, 3, 18, 1, 5, 0, 18, 9, 0, 8, 19, 3, 18, 0, 18, 10, 0, 67, 20, 13, 13, 14, 13, 14, 13, 14, 18, 5, 13, 14, 14, 43, 18, 3, 19, 1, 20, 0, 19, 11, 0, 70, 18, 14, 1, 19, 14, 20, 13, 14, 13, 13, 13, 13, 13, 13, 14, 71, 20, 14, 14, 13, 16, 13, 1, 13, 13, 18, 14, 13, 43, 16, 15, 17, 1, 18, 67, 15, 13, 13, 14, 13, 13, 14, 14, 16, 4, 13, 14, 13, 61, 14, 4, 14, 15, 0, 14, 14, 14, 13, 0, 15, 12, 0, 67, 16, 14, 14, 14, 14, 14, 13, 14, 15, 5, 14, 14, 14, 61, 14, 5, 14, 16, 1, 13, 14, 13, 14, 1, 6, 1388385388, 1, 7, 2931, 1, 8, 31585, 39, 729, 56, 2660, 2661, 2665, 398494316, 56, 2210, 2659, 2672, 3556561616, 45, 4, 0, 15, 0, 0, 45, 15, 20, 9, 17, 14, 42, 6, 9, 3, 6, 7, 8, 1, 10, 0, 8, 6, 6, 10, 58, 6, 0, 15, 13, 0, 0, 17, 14, 0, 0, 16, 15, 0, 47, 11, 1813, 2, 11, 0, 0, 4, 5, 0, 18, 4, 0, 61, 16, 6, 16, 18, 1, 17, 16, 16, 16, 0, 18, 5, 0, 61, 17, 7, 16, 18, 2, 16, 17, 15, 16, 62, 7, 15, 16, 15, 15, 15, 15, 16, 18, 15, 3, 15, 63, 18, 15, 15, 13, 17, 15, 15, 64, 17, 16, 16, 13, 15, 13, 17, 1, 9, 3376884460, 1, 14, 544943260, 65, 17, 14, 15, 15, 9, 17, 15, 14, 17, 66, 14, 15, 13, 17, 14, 43779, 15, 17, 67, 9, 15, 16, 15, 17, 15, 15, 15, 14, 9, 15, 16, 15, 1, 10, 3764612002, 39, 1119, 0, 18, 16, 0, 67, 19, 15, 15, 15, 15, 15, 17, 17, 18, 1, 15, 17, 15, 0, 18, 5, 0, 71, 18, 16, 15, 15, 19, 15, 2, 15, 15, 20, 16, 15, 61, 17, 1, 15, 20, 1, 17, 15, 17, 15, 0, 18, 17, 0, 72, 16, 15, 16, 1, 19, 18, 16, 15, 73, 17, 16, 1, 17, 16, 19, 15, 16, 16, 16, 15, 16, 15, 18, 15, 0, 19, 18, 0, 69, 15, 16, 15, 17, 15, 19, 18, 15, 15, 20, 15, 16, 15, 17, 17, 61, 15, 8, 17, 20, 2, 15, 17, 17, 15, 2, 18, 7, 0, 0, 19, 8, 0, 8, 20, 18, 19, 0, 19, 9, 0, 8, 21, 0, 19, 67, 19, 16, 16, 15, 17, 15, 15, 15, 7, 2, 15, 16, 15, 43, 22, 0, 21, 1, 19, 73, 15, 17, 22, 16, 17, 8, 16, 16, 15, 15, 15, 17, 15, 19, 15, 43, 21, 18, 20, 1, 19, 67, 18, 15, 17, 15, 15, 16, 16, 17, 21, 6, 16, 17, 16, 61, 15, 6, 16, 18, 2, 15, 16, 15, 15, 61, 16, 18, 17, 7, 1, 17, 16, 16, 16, 0, 18, 10, 0, 67, 19, 16, 17, 17, 16, 15, 16, 16, 18, 7, 16, 16, 15, 61, 16, 7, 16, 19, 0, 16, 15, 16, 16, 1, 9, 1272320661, 1, 10, 2691875819, 39, 1119, 45, 6, 0, 18, 0, 0, 45, 18, 5, 7, 19, 56, 2521, 2216, 2230, 1613010742, 42, 9, 11, 2, 10, 9, 0, 12, 19, 0, 8, 9, 9, 12, 58, 9, 0, 5, 20, 0, 0, 4, 21, 0, 4, 3, 2, 6, 22, 0, 0, 7, 23, 0, 8, 8, 6, 7, 0, 7, 24, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 5, 0, 72, 4, 4, 4, 9, 7, 6, 4, 4, 61, 4, 0, 5, 7, 1, 5, 4, 4, 4, 0, 6, 25, 0, 70, 0, 4, 2, 6, 4, 7, 5, 4, 4, 4, 5, 4, 5, 5, 4, 0, 6, 26, 0, 72, 4, 4, 5, 0, 8, 6, 5, 4, 71, 8, 5, 5, 4, 7, 5, 2, 5, 5, 6, 5, 4, 0, 7, 5, 0, 72, 4, 5, 4, 6, 8, 7, 5, 5, 61, 4, 0, 4, 8, 2, 4, 5, 5, 4, 0, 6, 27, 0, 72, 5, 5, 5, 0, 7, 6, 4, 4, 73, 4, 4, 0, 4, 5, 7, 4, 4, 4, 4, 5, 4, 4, 6, 4, 0, 7, 5, 0, 72, 4, 4, 5, 6, 8, 7, 5, 4, 61, 5, 0, 4, 8, 2, 4, 5, 4, 5, 0, 6, 28, 0, 65, 4, 0, 5, 5, 6, 5, 5, 7, 5, 0, 6, 5, 0, 72, 4, 5, 5, 7, 8, 6, 5, 5, 61, 5, 0, 4, 8, 2, 4, 4, 4, 4, 0, 6, 29, 0, 67, 7, 5, 5, 4, 5, 4, 5, 5, 6, 0, 4, 5, 5, 0, 6, 5, 0, 72, 5, 4, 5, 7, 8, 6, 4, 4, 61, 4, 0, 5, 8, 1, 4, 4, 4, 5, 67, 6, 4, 4, 5, 5, 5, 4, 4, 1, 0, 5, 5, 5, 0, 7, 5, 0, 72, 5, 5, 5, 6, 8, 7, 4, 5, 61, 4, 0, 4, 8, 2, 5, 5, 5, 5, 0, 6, 30, 0, 49, 7, 1, 6, 0, 45, 7, 0, 6, 0, 0, 45, 6, 55, 19, 16, 10, 18, 0, 7, 31, 0, 0, 6, 32, 0, 4, 4, 0, 8, 33, 0, 70, 2, 6, 0, 8, 7, 9, 7, 6, 6, 6, 6, 6, 6, 7, 6, 71, 9, 7, 6, 6, 1, 6, 0, 6, 6, 8, 7, 6, 0, 9, 5, 0, 72, 6, 6, 7, 8, 10, 9, 6, 7, 61, 6, 5, 7, 10, 1, 6, 7, 7, 7, 0, 8, 34, 0, 73, 7, 6, 0, 6, 6, 8, 7, 6, 6, 6, 7, 6, 6, 9, 6, 0, 8, 5, 0, 72, 7, 7, 7, 9, 10, 8, 7, 7, 61, 6, 0, 7, 10, 1, 6, 7, 7, 6, 0, 8, 25, 0, 70, 0, 6, 2, 8, 6, 9, 6, 7, 7, 6, 6, 6, 6, 7, 7, 0, 8, 26, 0, 72, 7, 7, 6, 0, 10, 8, 6, 7, 71, 10, 7, 6, 6, 9, 7, 0, 6, 6, 8, 6, 6, 0, 9, 5, 0, 72, 6, 6, 6, 8, 10, 9, 6, 6, 61, 6, 0, 6, 10, 1, 7, 6, 7, 6, 2, 8, 22, 0, 0, 9, 23, 0, 8, 10, 8, 9, 0, 9, 35, 0, 43, 11, 8, 10, 2, 0, 9, 0, 8, 5, 0, 72, 6, 6, 7, 11, 9, 8, 6, 6, 61, 6, 0, 6, 9, 1, 6, 7, 6, 6, 74, 6, 6, 6, 0, 7, 7, 6, 8, 6, 7, 6, 6, 0, 9, 5, 0, 72, 6, 6, 6, 8, 10, 9, 6, 7, 61, 6, 0, 6, 10, 0, 7, 6, 7, 7, 0, 8, 36, 0, 72, 6, 7, 6, 0, 9, 8, 7, 7, 73, 7, 7, 0, 7, 6, 9, 7, 7, 6, 7, 7, 6, 7, 8, 7, 0, 9, 5, 0, 72, 7, 7, 7, 8, 10, 9, 6, 6, 61, 7, 0, 7, 10, 0, 7, 6, 6, 6, 67, 8, 7, 6, 7, 7, 7, 7, 7, 5, 0, 7, 6, 7, 0, 9, 5, 0, 72, 6, 6, 6, 8, 10, 9, 7, 6, 61, 7, 0, 6, 10, 0, 6, 6, 7, 6, 48, 8, 1, 0, 45, 8, 32, 3, 17, 0, 8, 0, 0, 45, 8, 0, 6, 37, 0, 0, 5, 38, 0, 0, 4, 39, 0, 4, 3, 2, 7, 22, 0, 0, 8, 23, 0, 8, 9, 7, 8, 0, 8, 40, 0, 43, 10, 7, 9, 2, 1, 8, 0, 7, 5, 0, 72, 6, 4, 4, 10, 8, 7, 6, 6, 61, 4, 1, 4, 8, 2, 4, 5, 5, 5, 0, 7, 41, 0, 73, 5, 4, 1, 6, 6, 7, 6, 6, 5, 5, 4, 6, 4, 8, 4, 0, 7, 5, 0, 72, 4, 5, 4, 8, 9, 7, 6, 4, 61, 6, 1, 6, 9, 2, 4, 5, 4, 4, 0, 7, 42, 0, 72, 6, 6, 6, 1, 8, 7, 5, 5, 73, 6, 6, 1, 4, 4, 8, 5, 6, 5, 5, 6, 5, 6, 7, 6, 0, 8, 5, 0, 72, 4, 6, 5, 7, 9, 8, 6, 5, 61, 6, 1, 5, 9, 1, 6, 4, 5, 5, 67, 7, 6, 5, 4, 6, 5, 5, 5, 0, 1, 5, 6, 4, 0, 8, 5, 0, 72, 6, 6, 4, 7, 9, 8, 5, 5, 61, 4, 1, 5, 9, 0, 4, 6, 5, 6, 0, 7, 43, 0, 73, 6, 4, 1, 5, 4, 7, 4, 5, 4, 4, 5, 5, 4, 8, 4, 0, 7, 5, 0, 72, 6, 5, 5, 8, 9, 7, 6, 6, 61, 5, 1, 5, 9, 2, 4, 6, 6, 5, 0, 7, 25, 0, 70, 1, 5, 1, 7, 6, 8, 6, 5, 6, 6, 5, 5, 6, 4, 6, 0, 7, 26, 0, 72, 6, 4, 4, 1, 9, 7, 4, 6, 71, 9, 6, 4, 4, 8, 6, 2, 6, 6, 7, 5, 6, 0, 8, 5, 0, 72, 4, 4, 4, 7, 9, 8, 6, 6, 61, 6, 1, 6, 9, 2, 6, 6, 6, 6, 0, 7, 44, 0, 67, 8, 4, 4, 4, 4, 6, 4, 4, 7, 1, 6, 5, 5, 0, 7, 5, 0, 72, 4, 6, 6, 8, 9, 7, 5, 4, 61, 5, 1, 5, 9, 2, 5, 5, 5, 6, 0, 7, 19, 0, 49, 8, 1, 7, 1, 45, 8, 1, 10, 3742, 56, 2632, 2216, 2221, 3919497702, 0, 7, 0, 0, 45, 7, 13, 13, 9, 1, 26, 13, 0, 4, 4, 0, 56, 2522, 2670, 2678, 2718950464, 56, 2251, 2660, 2676, 3086758517, 11, 1, 24, 14, 36, 8, 6, 56, 2632, 2246, 2248, 261037462, 16, 2, 17, 13, 12, 7, 1, 5, 34, 14, 21, 1, 14, 45471, 13, 9, 21, 5, 16, 8, 3, 13, 56, 2228, 2518, 2521, 1253541291, 4, 18, 7, 0, 23, 32, 23, 23, 12, 5, 9, 21, 14, 8, 5, 8, 36, 16, 10, 22, 11, 11, 13, 19, 12, 16, 4, 3, 2736615432, 1095624944, 3749814314, 2109552640, 469143602, 3123189763, 1483178354, 4137355326, 2496696359, 856303633, 3511001116, 1870256235, 229798113, 2884421714, 1243824609, 3898591308, 2257858783, 616978474, 3271090235, 1630894131, 4285141119, 2644609492, 1004339902, 3658647756, 2017988714, 377749446, 3032440899, 1391503579, 4046336075, 2405779942, 765051057, 3419830488, 1778700246, 138369100, 2792595689, 1152133222, 3806499294, 2165901419, 525631712, 3180208046, 1539421093, 4194117819, 2553716925, 912812443, 3567776150, 1926977693, 286310537, 2940386289, 1300058285, 3954305153, 2313839005, 673507715, 1614778865, 1687664829, 46924728, 2701559792, 1060827290, 3715393890, 2074747377, 434223439, 3088920803, 1448270692, 4102047650, 2461780140, 821514421, 3475515839, 1835442521, 194844892, 2849108804, 1208907769, 3863249788, 2222917865, 582201673, 3236698590, 1596154184, 4250860344, 2610190138, 969526077, 3623469265, 1983203674, 342807016, 2996861302, 1356849382, 4011042656, 3395112668, 1754386721, 113916216, 2768544035, 1127947048, 3782702887, 2141457248, 501194464, 3155364161, 1514975731, 3892535227, 2252318787, 611463391, 3266091098, 1625358753, 4280187061, 2639532196, 998879144, 3652788307, 2012535014, 372133990, 3026450932, 1386168423, 4040345755, 1098803171, 3753187436, 2112401634, 472004724, 3126821368, 1486030947, 4140757214, 2500094040, 859415494, 3513333943, 1873039424, 232704463, 2886942805, 1246742735, 3900854388, 2260253683, 620044407, 3274545372, 1633808477, 4288514092, 2647982132, 1007192119, 3662032930, 2021365804, 380592356, 3034769515, 2145426107, 4048668789, 2408169492, 1042456050, 3518419653, 1877255730, 236921458, 2891165380, 38070003, 2692312401, 1051860456, 3706553712, 2065761009, 425369423, 3673353605, 2032695946, 392402584, 3047099292, 1406189155, 4061006582, 2420479603, 779746790, 3434504814, 1793382057, 153007022, 2807254974, 1167049381, 3821172331, 2180574673, 540367455, 3194889976, 1554098800, 4208795590, 2568391242, 927467215, 3582433806, 1941646798, 300983860, 2955027011, 1314746330, 3968987740, 2328512228, 688177777, 3342548454, 1702319726, 211623365, 2716237344, 1075374638, 3202507044, 2089433706, 448892447, 3103594011, 1462956574, 4116737660, 2476450814, 836187653, 3490230793, 1850138122, 209536514, 2863778425, 1223581234, 3877939718, 2237605403, 596875802, 3251375718, 1610806605, 4265542245, 2624884463, 983970396, 3638142503, 1997890088, 357498421, 3011531297, 1371523676, 4025728733, 2385005123, 744675320, 3399318130, 1758588618, 118102601, 2772742603, 1132138054, 3786900155, 2146170801, 2488486237, 4247819594, 2607427025, 967294306, 3621475571, 3697685204, 2057019238, 416219921, 3070396206, 1429999906, 4084312383, 2443780388, 803494619, 3457871711, 1817672668, 176932725, 2831567604, 1190703429, 3845466564, 19, 12, 24, 1, 56, 2244, 2255, 2264, 2141477605, 19, 14, 7, 9, 12, 19, 17, 6, 20, 3, 22, 23, 11, 4, 16, 20, 22, 0, 3, 8, 19, 14, 10, 17, 55, 1, 4036245398, 2395953851, 755224249, 3409738172, 1769132440, 128528027, 2783289988, 1142564750, 3796406926, 2156068492, 366320425, 3020492625, 1380367072, 4034666870, 2394384892, 753660226, 3408156402, 1767555907, 126959599, 2781696837, 1140714794, 3794821578, 2154490193, 514234070, 3168287600, 1528270841, 4182449012, 2541811435, 901409106, 3556090279, 1915299489, 274903681, 2929516454, 1288913813, 3943679632, 2302956191, 662088183, 3316195703, 1675991684, 35516310, 2689629065, 1049429632, 3703736973, 2063085955, 422816137, 13, 15, 11, 0, 18, 24, 9, 19, 56, 2244, 2528, 2541, 3116160462, 56, 2662, 2221, 2229, 237389171, 12, 19, 18, 13, 21, 16, 18, 5, 56, 2597, 2523, 2524, 190639656, 19, 20, 21, 16, 4, 1, 4, 15, 5, 1, 6, 15, 17, 15, 4, 21, 18, 11, 23, 55, 1, 24, 48118, 56, 2208, 2666, 2677, 1743343417, 4, 9, 2157899615, 517116913, 3171694442, 1531151079, 4185856337, 2545218002, 904292665, 3558448945, 1918184389, 277788490, 22, 14, 16, 3, 19, 0, 0, 16, 13, 9, 7, 10, 4, 16, 4, 5, 56, 2524, 2511, 2518, 1895101890, 35, 0, 21, 15, 1152505321, 3807149393, 2166811305, 526034857, 3180608433, 1540066978, 4194769578, 2554103190, 913182051, 3567358715]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 16; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_2_7_6_8_1_4_3_5_0 (order: [2,7,6,8,1,4,3,5,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[3]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[0]; + var pairCount = _operands[1]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[2]]; + var val = regs[base + _operands[3]]; + o[key] = val; + } + regs[base + dst] = o; + // BXOR + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a ^ regs[base + _operands[6]]; + // MOVE + var dst = _operands[7]; + regs[base + dst] = regs[base + _operands[8]]; + break; + } + case 62: + { + // ANTI_LT_7_5_1_2_4_11_10_9_6_0_8_3 (order: [7,5,1,2,4,11,10,9,6,0,8,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[5]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // DIV + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a / regs[base + _operands[5]]; + // LT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a < regs[base + _operands[8]]; + // SHR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a >> regs[base + _operands[11]]; + break; + } + case 63: + { + // ANTI_UNARY_NOT_1_6_4_0_3_2_5 (order: [1,6,4,0,3,2,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[1]]; + // UNARY_NOT + var dst = _operands[0]; + regs[base + dst] = !regs[base + _operands[1]]; + // LT + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a < regs[base + _operands[4]]; + // UNARY_NOT + var dst = _operands[5]; + regs[base + dst] = !regs[base + _operands[6]]; + break; + } + case 64: + { + // ANTI_UNARY_POS_0_1_3_6_4_5_2 (order: [0,1,3,6,4,5,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[3]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[4]]); + // UNARY_POS + var dst = _operands[5]; + regs[base + dst] = +regs[base + _operands[6]]; + break; + } + case 65: + { + // ANTI_SUB_7_4_8_6_5_0_2_3_1 (order: [7,4,8,6,5,0,2,3,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[2]]; + // BOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a | regs[base + _operands[2]]; + // SUB + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a - regs[base + _operands[5]]; + // BAND + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a & regs[base + _operands[8]]; + break; + } + case 66: + { + // ANTI_MUL_0_4_2_3_1_7_6_5 (order: [0,4,2,3,1,7,6,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[5]]; + // MUL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a * regs[base + _operands[2]]; + // LTE + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a <= regs[base + _operands[5]]; + // LOAD_INT + var dst = _operands[6]; + regs[base + dst] = _operands[7]; + break; + } + case 67: + { + // ANTI_ADD_3_9_8_1_11_0_10_12_5_4_2_6_7 (order: [3,9,8,1,11,0,10,12,5,4,2,6,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[7]]; + // SHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >> regs[base + _operands[2]]; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // UNARY_BITNOT + var dst = _operands[6]; + regs[base + dst] = ~regs[base + _operands[7]]; + // UNARY_BITNOT + var dst = _operands[8]; + regs[base + dst] = ~regs[base + _operands[9]]; + // SHR + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a >> regs[base + _operands[12]]; + break; + } + case 68: + { + // ANTI_UNARY_NEG_0_2_1_5_8_3_6_7_4 (order: [0,2,1,5,8,3,6,7,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[4]]; + // UNARY_NEG + var dst = _operands[0]; + regs[base + dst] = -regs[base + _operands[1]]; + // BAND + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a & regs[base + _operands[4]]; + // UNARY_BITNOT + var dst = _operands[5]; + regs[base + dst] = ~regs[base + _operands[6]]; + // TYPEOF + var dst = _operands[7]; + regs[base + dst] = typeof regs[base + _operands[8]]; + break; + } + case 69: + { + // ANTI_BAND_5_10_6_4_3_2_1_13_14_0_12_7_9_8_11 (order: [5,10,6,4,3,2,1,13,14,0,12,7,9,8,11]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[11], _unsortedOperands[13], _unsortedOperands[12], _unsortedOperands[1], _unsortedOperands[14], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[8]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // EQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a === regs[base + _operands[8]]; + // GT + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a > regs[base + _operands[11]]; + // EQ + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a === regs[base + _operands[14]]; + break; + } + case 70: + { + // ANTI_SHL_4_7_9_5_11_3_12_6_0_13_2_1_14_8_10 (order: [4,7,9,5,11,3,12,6,0,13,2,1,14,8,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[11], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[13], _unsortedOperands[2], _unsortedOperands[14], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[12]]; + // BOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a | regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // UNARY_NOT + var dst = _operands[6]; + regs[base + dst] = !regs[base + _operands[7]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[8]; + var pairCount = _operands[9]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[10]]; + var val = regs[base + _operands[11]]; + o[key] = val; + } + regs[base + dst] = o; + // EXP + // Math.pow instead of `**` + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[14]]); + break; + } + case 71: + { + // ANTI_BOR_11_7_5_4_10_3_1_0_6_9_8_2 (order: [11,7,5,4,10,3,1,0,6,9,8,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[0]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[0]; + var pairCount = _operands[1]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[2]]; + var val = regs[base + _operands[3]]; + o[key] = val; + } + regs[base + dst] = o; + // DIV + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a / regs[base + _operands[6]]; + // UNARY_NEG + var dst = _operands[7]; + regs[base + dst] = -regs[base + _operands[8]]; + // BOR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a | regs[base + _operands[11]]; + break; + } + case 72: + { + // ANTI_USHR_4_7_6_1_0_2_3_5 (order: [4,7,6,1,0,2,3,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[1]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // EQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a === regs[base + _operands[5]]; + // UNARY_BITNOT + var dst = _operands[6]; + regs[base + dst] = ~regs[base + _operands[7]]; + break; + } + case 73: + { + // ANTI_BXOR_9_4_7_12_3_8_1_13_2_0_5_10_11_6_14 (order: [9,4,7,12,3,8,1,13,2,0,5,10,11,6,14]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[13], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[14]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // MUL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a * regs[base + _operands[5]]; + // BXOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a ^ regs[base + _operands[8]]; + // EQ + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a === regs[base + _operands[11]]; + // MUL + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a * regs[base + _operands[14]]; + break; + } + case 74: + { + // ANTI_UNARY_BITNOT_11_8_1_3_5_4_6_2_10_0_9_7 (order: [11,8,1,3,5,4,6,2,10,0,9,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[11], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[0]]; + // UNARY_NOT + var dst = _operands[0]; + regs[base + dst] = !regs[base + _operands[1]]; + // UNARY_BITNOT + var dst = _operands[2]; + regs[base + dst] = ~regs[base + _operands[3]]; + // UNARY_BITNOT + var dst = _operands[4]; + regs[base + dst] = ~regs[base + _operands[5]]; + // SHL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a << regs[base + _operands[8]]; + // BAND + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a & regs[base + _operands[11]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-alias-specialized-dispatcher-1.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-alias-specialized-dispatcher-1.encoded.js new file mode 100644 index 00000000..2881442f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-alias-specialized-dispatcher-1.encoded.js @@ -0,0 +1,1662 @@ +var CONSTANTS = [/* 0 */1, /* 1 */"window", /* 2 */"CLEANED", /* 3 */true, /* 4 */undefined, /* 5 */0, /* 6 */14, /* 7 */18, /* 8 */2632122864, /* 9 */16, /* 10 */"Math", /* 11 */"imul", /* 12 */2693262067, /* 13 */"SAVED", /* 14 */"TEST_OUTPUT", /* 15 */7, /* 16 */"pauseGate"]; +var BYTECODE = [56, 5, 447, 468, 3910577072, 19, 11, 14, 10, 56, 351, 588, 599, 2539659440, 56, 351, 587, 592, 1408400625, 19, 11, 0, 15, 55, 4, 24, 56, 31, 424, 447, 769687594, 26, 14, 14, 10, 17, 19, 21, 9, 21, 6, 3, 22, 27, 12, 16, 10, 56, 587, 539, 552, 3377748332, 4, 5, 56, 59, 533, 534, 1370108972, 44, 56, 65, 634, 675, 1007220143, 56, 387, 604, 610, 620033970, 23, 16, 11, 2, 26, 11, 22, 24, 34, 15, 15, 26, 9, 22, 1, 35, 0, 12, 23, 7, 20, 22, 32, 13, 13, 27, 16, 12, 12, 56, 383, 337, 346, 1463903744, 4, 5, 56, 111, 401, 404, 1179167123, 5, 7, 15, 56, 119, 468, 476, 1672983383, 56, 587, 558, 562, 625512925, 5, 14, 24, 56, 132, 404, 424, 1615805824, 16, 10, 9, 1, 19, 12, 0, 7, 56, 565, 602, 617, 2116510484, 56, 396, 603, 604, 3695775982, 55, 45, 112, 0, 0, 8, 94, 113, 9, 3, 114, 198, 2, 55, 114, 1157761953, 5, 114, 3430322586, 6, 61, 55, 114, 199, 2, 114, 3321017146, 5, 114, 1266456452, 6, 61, 117, 1, 0, 8, 112, 0, 2, 9, 112, 0, 3, 10, 95, 96, 97, 62, 62, 112, 0, 4, 8, 116, 8, 42, 5, 7, 2, 6, 5, 98, 99, 100, 112, 0, 5, 5, 101, 113, 6, 1, 112, 0, 6, 4, 102, 112, 0, 7, 4, 103, 104, 112, 0, 5, 5, 105, 113, 6, 1, 112, 0, 8, 4, 106, 112, 0, 5, 4, 107, 113, 6, 1, 112, 0, 9, 4, 115, 4, 1, 5, 108, 112, 0, 5, 5, 115, 5, 4, 6, 113, 6, 1, 117, 10, 0, 4, 112, 0, 11, 5, 109, 112, 0, 12, 5, 43, 7, 4, 6, 2, 1, 5, 112, 0, 5, 4, 115, 4, 7, 5, 113, 5, 1, 110, 112, 0, 5, 5, 115, 5, 4, 6, 113, 6, 1, 111, 112, 0, 5, 5, 115, 5, 4, 6, 113, 6, 1, 116, 1, 112, 0, 4, 4, 116, 4, 56, 376, 358, 368, 4249343954, 13, 7, 16, 23, 21, 22, 20, 23, 56, 352, 624, 629, 4019635200, 32, 16, 23, 56, 603, 340, 347, 1392964937, 16, 13, 1, 20, 25, 6, 4, 7, 36, 3, 16, 11, 3, 3, 1, 20, 16, 2, 12, 34, 19, 6, 27, 21, 0, 4, 56, 590, 387, 399, 1076355419, 23, 19, 5, 2, 4, 10, 32, 3834054624, 2192804006, 552472671, 4269884126, 2629490128, 989101530, 3643533995, 2003248780, 362593202, 3017150351, 1376361123, 4031055560, 2390592939, 749736523, 3404637868, 1763910198, 123199392, 2777313942, 1136969471, 3791216077, 387089205, 510506707, 3164691040, 3423705467, 1783374298, 143042900, 2797294314, 1157089081, 3811197860, 2170609444, 530362712, 3184929120, 1544135036, 4198852886, 2558457656, 917510928, 3572469520, 1931688713, 291017013, 2945116421, 1304781068, 3959019266, 2318553872, 678182657, 3332555638, 1692354940, 2269781792, 629453604, 3283813644, 1643617537, 2886962, 2657580305, 1016797971, 3671371758, 2030713657, 390190795, 3044887915, 1404239248, 4058010916, 2417742626, 777502523, 3431473975, 1791399633, 150830428, 2805042580, 1164838154, 3819223740, 32449423, 2686691232, 1046467182, 3700653738, 2060371421, 419710290, 3074014927, 1433479969, 32, 1, 4, 15, 12, 11, 8, 23, 20, 2, 21, 12, 21, 12, 8, 24, 14, 21, 5, 13, 18, 3, 7, 21, 9, 12, 11, 21, 17, 24, 1, 36, 2, 9, 12, 13, 9, 17, 22, 2, 8, 7, 56, 555, 347, 349, 1209946206, 34, 1, 16, 8, 20, 9, 12, 32, 16, 1, 4024881809, 25, 7, 22, 21, 20, 5, 9, 7, 56, 375, 592, 608, 329238767, 56, 623, 375, 379, 1322630380, 16, 10, 17, 8, 56, 542, 588, 601, 2713225533, 55, 14, 21, 16, 8, 18, 0, 23, 13, 19, 3, 1, 20, 56, 557, 376, 387, 3975208476, 56, 349, 338, 349, 1631177435, 35, 6, 2, 8, 0, 21, 2, 18, 13, 9, 17, 56, 371, 607, 617, 702799838, 35, 3, 16, 55, 56, 356, 384, 395, 2769174992, 27, 0, 21, 22, 56, 591, 550, 560, 359116224, 12, 0, 12, 13, 25, 12, 19, 1, 22, 5, 21, 19, 5, 4, 17, 2, 3662069225, 2020879023, 380622434, 3034807275, 1394396794, 4048706212, 2408134104, 767929824, 3422184078, 1781589688, 141259708, 2796003232, 1155007111, 3809904362, 2169314701, 528510331, 3183339254, 1542282124, 4196478879, 2556082835, 915687140, 3570126282, 1929870763, 289199780, 2943787874, 1302937490, 3957691239, 2317227752, 676396394, 3331263876, 1690539387, 49822411, 2703929273, 1063705532, 3717756621, 2077609232, 437083575, 3091252556, 1451111165, 4105461578, 2465194887]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // JUMP_205 (specialized) + regs[fp + SLOTS.PC] = 205; + break; + } + case 62: + { + // RETURN_3 (specialized) + var retVal = regs[base + 3]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 63: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 64: + { + // MOVE_2_4 (specialized) + var dst = 2; + regs[base + dst] = regs[base + 4]; + break; + } + case 65: + { + // MOVE_3_4 (specialized) + var dst = 3; + regs[base + dst] = regs[base + 4]; + break; + } + case 66: + { + // LOAD_GLOBAL_4_1_0 (specialized) + var dst = 4; + var globalName = this._constant(1, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 67: + { + // LOAD_CONST_5_14_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(14, 0); + break; + } + case 68: + { + // LOAD_GLOBAL_7_1_0 (specialized) + var dst = 7; + var globalName = this._constant(1, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 69: + { + // LOAD_CONST_8_2_0 (specialized) + var dst = 8; + regs[base + dst] = this._constant(2, 0); + break; + } + case 70: + { + // GET_PROP_9_7_8 (specialized) + // dst = regs[obj][regs[key]] + var dst = 9; + var obj = regs[base + 7]; + var key = regs[base + 8]; + regs[base + dst] = obj[key]; + break; + } + case 71: + { + // LOAD_GLOBAL_7_1_0 (specialized) + var dst = 7; + var globalName = this._constant(1, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 72: + { + // LOAD_CONST_8_13_0 (specialized) + var dst = 8; + regs[base + dst] = this._constant(13, 0); + break; + } + case 73: + { + // GET_PROP_10_7_8 (specialized) + // dst = regs[obj][regs[key]] + var dst = 10; + var obj = regs[base + 7]; + var key = regs[base + 8]; + regs[base + dst] = obj[key]; + break; + } + case 74: + { + // SET_PROP_4_5_7 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 4]; + var key = regs[base + 5]; + var val = regs[base + 7]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 75: + { + // LOAD_CONST_4_4_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(4, 0); + break; + } + case 76: + { + // RETURN_4 (specialized) + var retVal = regs[base + 4]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 77: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 78: + { + // MOVE_2_3 (specialized) + var dst = 2; + regs[base + dst] = regs[base + 3]; + break; + } + case 79: + { + // LOAD_GLOBAL_3_1_0 (specialized) + var dst = 3; + var globalName = this._constant(1, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 80: + { + // SET_PROP_3_4_5 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 3]; + var key = regs[base + 4]; + var val = regs[base + 5]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 81: + { + // GET_PROP_5_3_4 (specialized) + // dst = regs[obj][regs[key]] + var dst = 5; + var obj = regs[base + 3]; + var key = regs[base + 4]; + regs[base + dst] = obj[key]; + break; + } + case 82: + { + // RETURN_2 (specialized) + var retVal = regs[base + 2]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 83: + { + // RETURN_3 (specialized) + var retVal = regs[base + 3]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 84: + { + // LOAD_UPVALUE_0_0 (specialized) + var dst = 0; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 85: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 86: + { + // FINALLY_SETUP_182_2_3_196 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 182, + contReg: 2, + payloadReg: 3, + throwPad: 196 + }); + break; + } + case 87: + { + // TRY_SETUP_152_4 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 152, + exceptionReg: 4 + }); + break; + } + case 88: + { + // LOAD_UPVALUE_8_0 (specialized) + var dst = 8; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 89: + { + // MOVE_3_9 (specialized) + var dst = 3; + regs[base + dst] = regs[base + 9]; + break; + } + case 90: + { + // LOAD_INT_2_197 (specialized) + var dst = 2; + regs[base + dst] = 197; + break; + } + case 91: + { + // LOAD_INT_5_1863660266 (specialized) + var dst = 5; + regs[base + dst] = 1863660266; + break; + } + case 92: + { + // LOAD_INT_6_1835591523 (specialized) + var dst = 6; + regs[base + dst] = 1835591523; + break; + } + case 93: + { + // LOAD_INT_5_844598701 (specialized) + var dst = 5; + regs[base + dst] = 844598701; + break; + } + case 94: + { + // UNARY_NEG_9_8 (specialized) + var dst = 9; + regs[base + dst] = -regs[base + 8]; + break; + } + case 95: + { + // SET_PROP_8_9_10 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 8]; + var key = regs[base + 9]; + var val = regs[base + 10]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 96: + { + // JUMP_REG_2 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 2]; + break; + } + case 97: + { + // THROW_3 (specialized) + throw regs[base + 3]; + break; + } + case 98: + { + // JUMP_REG_5 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 5]; + break; + } + case 99: + { + // LOAD_THIS_3 (specialized) + var dst = 3; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 100: + { + // UNARY_BITNOT_4_1 (specialized) + var dst = 4; + regs[base + dst] = ~regs[base + 1]; + break; + } + case 101: + { + // USHR_6_4_5 (specialized) + var dst = 6; + var a = regs[base + 4]; + regs[base + dst] = a >>> regs[base + 5]; + break; + } + case 102: + { + // SHL_5_1_4 (specialized) + var dst = 5; + var a = regs[base + 1]; + regs[base + dst] = a << regs[base + 4]; + break; + } + case 103: + { + // USHR_6_1_4 (specialized) + var dst = 6; + var a = regs[base + 1]; + regs[base + dst] = a >>> regs[base + 4]; + break; + } + case 104: + { + // BOR_4_5_6 (specialized) + var dst = 4; + var a = regs[base + 5]; + regs[base + dst] = a | regs[base + 6]; + break; + } + case 105: + { + // USHR_6_4_5 (specialized) + var dst = 6; + var a = regs[base + 4]; + regs[base + dst] = a >>> regs[base + 5]; + break; + } + case 106: + { + // ADD_5_1_4 (specialized) + var dst = 5; + var a = regs[base + 1]; + regs[base + dst] = a + regs[base + 4]; + break; + } + case 107: + { + // USHR_6_5_4 (specialized) + var dst = 6; + var a = regs[base + 5]; + regs[base + dst] = a >>> regs[base + 4]; + break; + } + case 108: + { + // BXOR_4_1_5 (specialized) + var dst = 4; + var a = regs[base + 1]; + regs[base + dst] = a ^ regs[base + 5]; + break; + } + case 109: + { + // GET_PROP_6_4_5 (specialized) + // dst = regs[obj][regs[key]] + var dst = 6; + var obj = regs[base + 4]; + var key = regs[base + 5]; + regs[base + dst] = obj[key]; + break; + } + case 110: + { + // ADD_4_1_0 (specialized) + var dst = 4; + var a = regs[base + 1]; + regs[base + dst] = a + regs[base + 0]; + break; + } + case 111: + { + // UNARY_BITNOT_4_1 (specialized) + var dst = 4; + regs[base + dst] = ~regs[base + 1]; + break; + } + case 112: + { + // ALIAS_LOAD_CONST_2_1_0 (order: [2,1,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 113: + { + // ALIAS_MOVE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + break; + } + case 114: + { + // ALIAS_LOAD_INT_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + break; + } + case 115: + { + // ALIAS_USHR_2_1_0 (order: [2,1,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + break; + } + case 116: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 117: + { + // ALIAS_LOAD_GLOBAL_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-1.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-1.encoded.js new file mode 100644 index 00000000..c1cc4d63 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-1.encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */7, /* 1 */"window", /* 2 */"SAVED", /* 3 */undefined, /* 4 */"CLEANED", /* 5 */true, /* 6 */0, /* 7 */14, /* 8 */18, /* 9 */2632122864, /* 10 */16, /* 11 */"Math", /* 12 */"imul", /* 13 */2693262067, /* 14 */"TEST_OUTPUT", /* 15 */1, /* 16 */"pauseGate"]; +var BYTECODE = [56, 5, 480, 482, 2324228243, 4, 9, 56, 12, 536, 599, 460994956, 56, 777, 639, 640, 2947127960, 15, 19, 1, 9, 34, 21, 19, 56, 744, 791, 792, 1445349015, 25, 15, 11, 14, 34, 3, 23, 4, 14, 32, 3, 23, 55, 11, 24, 15, 11, 56, 622, 733, 746, 2781205869, 18, 14, 10, 15, 23, 21, 9, 18, 35, 3, 22, 27, 12, 16, 10, 56, 747, 620, 633, 3377748332, 23, 5, 7, 18, 56, 80, 602, 616, 146542554, 56, 711, 708, 723, 2882603563, 56, 619, 768, 779, 1754103196, 26, 11, 22, 24, 45, 4, 4, 1, 0, 3, 0, 0, 5, 2, 3, 2, 3, 1, 0, 0, 4, 2, 0, 47, 5, 156, 0, 1, 1, 0, 1, 2, 9, 3, 4, 5, 56, 131, 685, 697, 2553376020, 12, 22, 1, 11, 56, 710, 647, 655, 3474212842, 36, 10, 4, 43, 4, 3, 5, 0, 45, 2, 0, 3, 3, 0, 45, 3, 56, 161, 599, 602, 4129069473, 34, 12, 12, 45, 0, 56, 637, 711, 720, 974516781, 56, 176, 662, 671, 1179167123, 36, 15, 14, 56, 622, 717, 730, 2081029858, 0, 59, 268, 2, 3, 286, 54, 229, 4, 3, 8, 0, 42, 9, 8, 0, 55, 5, 3, 9, 1, 2, 288, 55, 1, 5, 3915599085, 1, 6, 1835591523, 39, 303, 8, 9, 15, 12, 55, 1, 5, 3817194794, 1, 6, 2535673501, 39, 303, 56, 234, 671, 685, 2793647782, 55, 5, 15, 24, 15, 19, 12, 0, 27, 1, 8, 20, 4, 9, 1, 5, 3151242662, 1, 6, 3430322586, 39, 303, 55, 1, 2, 297, 1, 5, 3282357564, 1, 6, 1266456452, 39, 303, 2, 8, 1, 0, 0, 9, 4, 0, 0, 10, 5, 0, 9, 8, 9, 10, 58, 2, 46, 3, 45, 3, 45, 3, 56, 628, 633, 642, 2699531026, 0, 8, 3, 0, 45, 8, 42, 5, 7, 2, 6, 5, 58, 5, 4, 3, 35, 4, 1, 0, 5, 6, 0, 21, 6, 4, 5, 5, 1, 6, 0, 4, 7, 0, 19, 5, 1, 4, 0, 4, 8, 0, 21, 6, 1, 4, 17, 4, 5, 6, 0, 5, 6, 0, 21, 6, 4, 5, 5, 1, 6, 0, 4, 9, 0, 11, 5, 1, 4, 0, 4, 6, 0, 21, 6, 5, 4, 5, 1, 6, 0, 4, 10, 0, 21, 5, 1, 4, 18, 4, 1, 5, 0, 5, 6, 0, 21, 6, 4, 5, 5, 1, 6, 2, 4, 11, 0, 0, 5, 12, 0, 8, 6, 4, 5, 0, 5, 13, 0, 43, 7, 4, 6, 2, 1, 5, 0, 4, 6, 0, 21, 5, 7, 4, 5, 1, 5, 11, 4, 1, 0, 0, 5, 6, 0, 21, 6, 4, 5, 5, 1, 6, 35, 4, 1, 0, 5, 6, 0, 21, 6, 4, 5, 5, 1, 6, 45, 1, 0, 4, 3, 0, 45, 4, 12, 21, 12, 8, 56, 784, 623, 637, 1781256496, 683746376, 3337743287, 56, 633, 628, 640, 2017153783, 21, 17, 24, 1, 36, 2, 9, 12, 13, 9, 17, 22, 2, 8, 7, 56, 769, 713, 715, 1209946206, 34, 1, 16, 8, 20, 9, 12, 19, 1, 15, 9, 22, 22, 8, 18, 8, 21, 20, 11, 34, 22, 18, 56, 697, 515, 526, 2751943061, 4, 1, 3115613851, 1475056575, 4129757134, 2489090725, 848170599, 3502347758, 1862138491, 221671165, 2875775599, 1235662745, 3889908415, 2249711287, 608847125, 3263493513, 1622761102, 4277543583, 2637014915, 996217733, 3651047163, 2009859468, 369591703, 3023769239, 1383381676, 4037683121, 2397137848, 756940462, 3411167907, 1770571237, 130235747, 2784973565, 1144248130, 3798885357, 2158292829, 517509841, 3171562935, 1531284921, 4185463498, 2545088324, 904686529, 3559104329, 1918839488, 278181243, 2932792774, 1291930972, 3946694358, 2306233126, 665366329, 3320259391, 1679533356, 38792669, 2692906336, 1052706539, 3706751868, 2066628585, 426093340, 3080283412, 1440152852, 4094455100, 2454181138, 813384450, 3467923203, 1827328777, 186742542, 2488315930, 847910958, 3502604339, 2801188591, 1160326051, 3815073701, 2174614183, 533827237, 3188529583, 1547862419, 4201647848, 2561380980, 921135093, 3575177871, 1935040156, 294498693, 2948712845, 56, 717, 752, 763, 2247284674, 56, 781, 730, 746, 15922566, 56, 735, 783, 789, 403093660, 25, 19, 19, 5, 56, 734, 626, 635, 1340886622, 25, 15, 16, 5, 36, 5, 6, 8, 3, 8, 1, 56, 789, 622, 626, 1322630380, 16, 10, 17, 8, 55, 31, 3834054386, 2192804142, 552472577, 3206768945, 1566368568, 4220674005, 2580034389, 939898572, 3594338682, 1152978396, 3807753763, 2167280168, 526478908, 3181175297, 1540511279, 4195340819, 2554052074, 913786489, 3567831804, 1927708177, 287173137, 2941364021, 1301166655, 912872469, 3567698953, 1926515837, 286252033, 2940449794, 1299983387, 3954351128, 2313778230, 673575989, 3327794229, 1687194644, 46871582, 56, 757, 649, 662, 3251295801, 56, 731, 622, 626, 2475367674, 56, 706, 732, 742, 4249343954, 13, 7, 16, 23, 21, 22, 20, 23, 56, 720, 791, 796, 4019635200, 32, 16, 23, 56, 766, 702, 709, 1392964937, 16, 13, 1, 20, 36, 4, 7, 36, 3, 16, 17, 8, 3, 3, 23, 1, 5, 15, 55, 14, 21, 16, 8, 18, 0, 23, 13, 19, 3, 1, 20, 56, 771, 708, 719, 3975208476, 56, 718, 698, 709, 1631177435, 35, 2, 24, 55, 5, 9, 15, 12, 17, 3, 7, 19, 13, 1, 4, 35, 3, 16, 55, 56, 728, 639, 650, 2769174992, 20, 23, 0, 21]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-2.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-2.encoded.js new file mode 100644 index 00000000..bd9b7fac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-2.encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */7, /* 1 */"window", /* 2 */"SAVED", /* 3 */undefined, /* 4 */1, /* 5 */"CLEANED", /* 6 */true, /* 7 */16, /* 8 */0, /* 9 */21, /* 10 */3356898789, /* 11 */"Math", /* 12 */"imul", /* 13 */3153583793, /* 14 */"TEST_OUTPUT", /* 15 */"pauseGate"]; +var BYTECODE = [56, 5, 659, 671, 3871796980, 13, 18, 6, 1, 16, 20, 21, 2, 23, 18, 12, 9, 56, 22, 600, 616, 1095218337, 20, 7, 5, 2, 12, 16, 0, 2, 26, 20, 24, 3, 4, 3, 4, 6, 56, 43, 707, 758, 1382922486, 1, 14, 29679, 12, 14, 8, 19, 14, 1, 2, 17, 13, 5, 20, 1, 13, 20, 7, 21, 56, 565, 679, 689, 3526646498, 17, 15, 3, 23, 56, 640, 499, 510, 3778016123, 20, 8, 7, 1, 25, 3, 2, 5, 16, 13, 1, 11, 34, 12, 16, 5, 24, 16, 45, 4, 4, 1, 0, 3, 0, 0, 5, 2, 3, 2, 3, 1, 0, 0, 4, 2, 0, 47, 5, 162, 0, 1, 1, 0, 1, 2, 9, 3, 4, 5, 2, 3, 1, 0, 56, 135, 758, 766, 911099022, 56, 593, 513, 520, 3750934859, 34, 12, 24, 43, 4, 3, 5, 0, 45, 2, 0, 3, 3, 0, 45, 3, 56, 490, 578, 590, 2094379805, 55, 56, 167, 704, 707, 3286118781, 34, 16, 22, 45, 0, 16, 11, 9, 23, 56, 181, 527, 536, 3950214336, 56, 688, 537, 544, 444113078, 22, 16, 13, 17, 59, 287, 2, 3, 305, 54, 235, 4, 3, 8, 0, 42, 9, 8, 0, 55, 5, 3, 9, 1, 2, 307, 55, 1, 5, 1371133225, 1, 6, 824348626, 39, 324, 56, 676, 590, 599, 2777196345, 55, 1, 5, 2192126969, 1, 6, 30518164, 39, 324, 0, 8, 4, 0, 32, 9, 8, 5, 3, 9, 1, 2, 309, 56, 253, 485, 489, 3397930814, 25, 13, 13, 13, 1, 6, 4037709480, 39, 324, 25, 2, 4, 19, 27, 20, 10, 12, 56, 275, 766, 776, 569619644, 23, 4, 2, 4, 56, 503, 643, 650, 460101409, 21, 39, 324, 2, 8, 1, 0, 0, 9, 5, 0, 0, 10, 6, 0, 9, 8, 9, 10, 58, 2, 46, 3, 45, 3, 45, 3, 32, 16, 18, 21, 3, 3, 0, 0, 8, 3, 0, 45, 8, 42, 5, 7, 2, 6, 5, 58, 5, 4, 3, 0, 4, 7, 0, 19, 5, 1, 4, 0, 4, 7, 0, 21, 6, 1, 4, 17, 4, 5, 6, 0, 5, 8, 0, 21, 6, 4, 5, 5, 1, 6, 18, 4, 1, 0, 0, 5, 8, 0, 21, 6, 4, 5, 5, 1, 6, 0, 4, 9, 0, 21, 5, 1, 4, 18, 4, 1, 5, 0, 5, 8, 0, 21, 6, 4, 5, 5, 1, 6, 0, 4, 10, 0, 11, 5, 1, 4, 0, 4, 8, 0, 21, 6, 5, 4, 5, 1, 6, 35, 4, 1, 0, 5, 8, 0, 21, 6, 4, 5, 5, 1, 6, 2, 4, 11, 0, 0, 5, 12, 0, 8, 6, 4, 5, 0, 5, 13, 0, 43, 7, 4, 6, 2, 1, 5, 0, 4, 8, 0, 21, 5, 7, 4, 5, 1, 5, 45, 1, 0, 4, 3, 0, 45, 4, 35, 2, 23, 25, 17, 18, 10, 1757187253, 116880526, 2771096990, 329726263, 32, 16, 0, 25, 5, 3, 9, 12, 10, 12, 16, 4, 14, 12, 11, 15, 19, 56, 503, 502, 505, 72918945, 8, 5, 4, 0, 56, 624, 690, 698, 2249968749, 23, 2, 15, 16, 55, 55, 18, 2309929814, 669208819, 3323975715, 1683112400, 42518876, 2697285833, 1056319290, 3710493507, 2070223041, 56, 650, 597, 598, 2689170, 4, 24, 34, 16, 1, 26, 20, 11, 23, 56, 631, 505, 519, 142315838, 24, 13, 7, 22, 14, 8, 0, 23, 14, 19, 21, 22, 34, 16, 15, 27, 5, 6, 8, 55, 24, 4, 19, 9, 13, 21, 11, 24, 5, 17, 0, 23, 16, 7, 8, 5, 4, 18, 19, 2, 14, 23, 55, 4, 12, 3749303718, 2109031832, 468647459, 3122678455, 1482676151, 4136844200, 2496179878, 855775842, 3510400493, 1869680231, 229212902, 2883860076, 1243263961, 3898023507, 2256790193, 616461757, 1, 7, 59113, 36, 22, 2, 56, 578, 635, 646, 1594840788, 4, 16, 25, 4, 13, 4, 12, 22, 14, 18, 56, 623, 536, 551, 447809097, 5, 16, 13, 55, 16, 22, 3, 16, 32, 1, 12, 16, 15, 16, 11, 4, 3, 2230996829, 590674893, 3245037401, 1604724983, 4259029260, 2618763737, 977965404, 3632560300, 1991869347, 351344547, 3006050479, 1365387420, 25, 10, 12, 1, 14, 5, 14, 11, 16, 7, 5, 22, 16, 5, 3, 6, 32, 24, 5, 11, 7, 18, 22, 13, 20, 22, 8, 26, 23, 13, 20, 55, 21, 1645656893, 5056293, 2659216161, 4037195711, 2396576431, 756371111, 3410871709, 1770018120, 129674977, 2784437119, 1143711721, 3798340436, 2157739731, 516883886, 3171059122, 1530674869, 4184976192, 2544461791, 904198983, 3558502088, 1918358900, 277562817, 2932231488, 1291375325, 3946149717, 2305680176, 664805180, 3319706922, 1678980399, 38247774, 2692352737, 1052211054, 3706395630, 2066132840, 425469202, 3079787838, 1439255864, 4093967669, 2453555005, 812897032, 3467624237, 1826759425, 186180867, 2840952313, 1199934723, 3854042881, 2213722901, 573509398, 3227753266, 1587537211, 4241714473, 2601049378, 960672999, 3615245167, 3565361647, 1925096086, 284440222, 2938721164, 1298255795, 3952886402, 2312558223, 671830405, 3224186374, 1583372853, 4238198281, 2597669665, 956882437, 3611699716, 2389259726, 330243594, 2984473089, 598201187]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-4.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-4.encoded.js new file mode 100644 index 00000000..ce0b3721 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-dispatcher-4.encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */7, /* 1 */"window", /* 2 */"SAVED", /* 3 */undefined, /* 4 */1, /* 5 */"CLEANED", /* 6 */true, /* 7 */"j7ntkm", /* 8 */16, /* 9 */0, /* 10 */2183977359, /* 11 */21, /* 12 */"Math", /* 13 */"imul", /* 14 */3966987261, /* 15 */"TEST_OUTPUT", /* 16 */"pauseGate"]; +var BYTECODE = [56, 5, 549, 624, 2563626569, 19, 21, 17, 24, 15, 0, 23, 3, 13, 8, 7, 10, 56, 746, 682, 688, 2287335883, 17, 17, 24, 18, 56, 674, 501, 515, 2105039278, 8, 18, 13, 8, 20, 6, 15, 24, 56, 679, 780, 788, 1036204084, 56, 723, 671, 672, 2840227923, 56, 496, 749, 765, 1201192855, 1, 9, 428, 17, 12, 0, 15, 23, 20, 20, 1, 22, 15, 19, 4, 27, 19, 3, 17, 16, 18, 8, 16, 55, 55, 22, 56, 85, 641, 645, 3054217796, 5, 14, 24, 47, 45, 4, 32, 22, 22, 56, 748, 681, 689, 2806603115, 4, 1, 0, 3, 0, 0, 5, 2, 3, 2, 3, 1, 0, 0, 4, 2, 0, 47, 5, 164, 0, 1, 1, 0, 1, 2, 9, 3, 4, 5, 56, 134, 627, 639, 3038916554, 18, 20, 4, 10, 24, 8, 20, 22, 34, 24, 12, 33, 43, 4, 3, 5, 0, 45, 2, 56, 663, 667, 674, 661125498, 0, 3, 3, 0, 45, 3, 56, 169, 624, 627, 2802978938, 5, 19, 5, 45, 0, 56, 179, 542, 549, 313473117, 32, 20, 18, 20, 5, 11, 21, 56, 191, 639, 641, 325058399, 4, 1, 59, 294, 2, 3, 312, 54, 239, 4, 3, 10, 0, 42, 11, 10, 0, 55, 5, 3, 11, 1, 2, 314, 55, 1, 5, 3985116657, 1, 6, 54115, 1, 7, 48228, 39, 338, 55, 1, 5, 1180700596, 1, 6, 57686, 1, 7, 60995, 39, 338, 0, 10, 4, 0, 32, 11, 10, 5, 3, 11, 1, 2, 321, 55, 56, 258, 728, 737, 162363457, 56, 748, 769, 774, 3134859523, 27, 20, 8, 15, 39, 338, 55, 1, 2, 323, 56, 278, 690, 693, 443894372, 1, 6, 49141, 1, 6, 392, 1, 7, 30890, 39, 338, 56, 660, 742, 755, 3734353500, 2, 10, 1, 0, 0, 11, 5, 0, 0, 12, 6, 0, 9, 10, 11, 12, 58, 2, 46, 3, 45, 3, 56, 500, 652, 666, 1319319552, 45, 3, 0, 10, 3, 0, 45, 10, 56, 707, 695, 710, 1245830207, 19, 4, 11, 4, 42, 5, 8, 3, 5, 6, 7, 0, 9, 7, 0, 8, 5, 5, 9, 58, 5, 4, 4, 0, 6, 8, 0, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 9, 0, 21, 8, 6, 7, 5, 5, 8, 0, 6, 10, 0, 11, 7, 0, 6, 0, 6, 9, 0, 21, 8, 7, 6, 5, 0, 8, 0, 6, 11, 0, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 9, 0, 21, 8, 6, 7, 5, 0, 8, 18, 6, 0, 5, 0, 7, 9, 0, 21, 8, 6, 7, 5, 0, 8, 2, 6, 12, 0, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 14, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 9, 0, 21, 7, 9, 6, 5, 0, 7, 0, 6, 7, 0, 49, 7, 1, 6, 0, 45, 7, 0, 6, 3, 0, 45, 6, 23, 19, 22, 8, 17, 14, 10, 8, 18, 15, 18, 3, 27, 14, 4, 9, 13, 8, 13, 22, 19, 10, 20, 4, 25, 1, 2, 3, 56, 495, 522, 529, 3230404913, 55, 56, 495, 646, 656, 3998735556, 26, 21, 22, 15, 22, 19, 24, 1, 8, 12, 19, 15, 36, 4, 17, 2967680445, 1327256729, 3981626839, 2341028793, 700831653, 3355061412, 1714470241, 923068459, 3577372761, 1937234170, 296431721, 2951104915, 1310244983, 3965012172, 2324557913, 683695045, 3338575943, 1697855677, 57119168, 2711226470, 1071087837, 3725265122, 2085000183, 444348543, 3098648812, 1458137177, 4112839131, 2472429741, 831766693, 3486497702, 1845645225, 205052058, 2859819155, 1218832883, 3872919665, 2232584436, 592380815, 3246628752, 1606414468, 4260587691, 2619924920, 979544463, 3634114695, 1993463960, 352931722, 3007642481, 1366970610, 4021758319, 2380505488, 740173984, 3394277550, 1754103720, 113607598, 2767718573, 1127657809, 3781896662, 2141217141, 500955390, 3155652468, 1514724297, 4169549654, 2528921806, 888262983, 3542182218, 1901912263, 261529439, 2915775442, 1275574116, 3929669880, 2289076594, 648901092, 3303381336, 1662529303, 22199077, 2676938530, 1036012322, 3690971427, 2050181399, 409522461, 3063560437, 1423287163, 4077512459, 1162584259, 3817216077, 2176610365, 1398485962, 4053184052, 2412569138, 771843632, 3425824341, 1785486046, 145147519, 2799225851, 1159159406, 3813403364, 2172675615, 532356643, 2979198246, 1338805540, 1413579219, 4068281939, 2427884207, 787015609, 20, 20, 4, 4, 34, 8, 21, 27, 10, 10, 3, 19, 12, 21, 20, 17, 20, 18, 0, 21, 7, 11, 8, 36, 12, 9, 27, 6, 18, 11, 1, 1, 61465, 5, 19, 5, 36, 8, 6, 21, 21, 2, 11, 4, 23, 3098083404, 1457949120, 3696314878, 35, 15, 18, 25, 23, 17, 2, 25, 3, 7, 18, 8, 22, 0, 19, 26, 12, 1, 17, 56, 673, 507, 517, 1654613475, 27, 24, 8, 2, 4, 20, 14, 15, 3, 21, 25, 2816593370, 1176394835, 1119023922, 2190439549, 549707760, 3204416463, 1563632861, 4218330182, 2577725185, 14, 0, 12, 13, 5, 20, 10, 17, 1, 15, 6, 1, 23, 55874, 12, 4, 16, 12, 56, 685, 666, 667, 2955570959, 8, 7, 19, 9, 21, 0, 5, 12, 25, 1, 11, 15, 18, 7, 17, 19, 27, 19, 4, 24, 8, 23, 11, 3, 56, 778, 742, 753, 4022701514, 20, 8, 9, 16]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-patch-232.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-patch-232.encoded.js new file mode 100644 index 00000000..caea33ef --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers-patch-232.encoded.js @@ -0,0 +1,1549 @@ +var CONSTANTS = [/* 0 */6850, /* 1 */11268, /* 2 */15144, /* 3 */"GiU7vgaF", /* 4 */"window", /* 5 */"SAVED", /* 6 */"pauseGate", /* 7 */undefined, /* 8 */63859, /* 9 */46027, /* 10 */"hlB8vETk7", /* 11 */1, /* 12 */"HonGdjEh", /* 13 */"CLEANED", /* 14 */true, /* 15 */15654, /* 16 */7, /* 17 */43208, /* 18 */56526, /* 19 */"rMIZSCQ", /* 20 */"p1cUi1qRt", /* 21 */"TEST_OUTPUT"]; +var BYTECODE = [56, 5, 319, 320, 3783700630, 11, 56, 11, 503, 570, 941638544, 56, 487, 492, 493, 3731946367, 36, 22, 8, 56, 422, 464, 468, 2380464165, 56, 576, 462, 475, 2609125613, 56, 582, 449, 456, 2398247804, 25, 15, 21, 0, 25, 12, 16, 15, 22, 2, 4, 14, 12, 22, 22, 4, 18, 12, 18, 23, 18, 9, 23, 22, 25, 13, 9, 1, 27, 6, 23, 20, 56, 469, 491, 492, 2503237519, 56, 382, 463, 471, 3354073485, 4, 13, 56, 83, 320, 337, 1733870543, 1, 5, 60672, 34, 20, 5, 56, 489, 407, 420, 2643306495, 20, 11, 21, 8, 55, 14, 95, 3, 0, 0, 4, 1, 0, 97, 0, 2, 5, 56, 116, 337, 342, 684599855, 8, 19, 12, 8, 15, 61, 5, 5, 2, 3, 0, 4, 4, 3, 6, 5, 5, 5, 5, 5, 78, 97, 0, 5, 7, 47, 8, 170, 0, 1, 1, 0, 1, 2, 79, 96, 6, 4, 0, 7, 6, 0, 80, 43, 7, 6, 8, 0, 81, 97, 0, 7, 6, 82, 83, 98, 0, 95, 6, 8, 0, 5, 9, 0, 84, 85, 86, 87, 42, 8, 7, 0, 55, 61, 6, 6, 3, 10, 0, 5, 6, 5, 8, 5, 5, 6, 6, 5, 88, 55, 63, 55, 89, 97, 0, 11, 7, 62, 7, 6, 5, 5, 8, 5, 5, 5, 61, 5, 6, 3, 12, 0, 5, 6, 6, 8, 6, 6, 5, 5, 6, 90, 55, 63, 55, 91, 63, 96, 7, 4, 0, 8, 13, 0, 97, 0, 14, 9, 92, 93, 94, 64, 64, 97, 0, 7, 7, 98, 7, 11, 24, 24, 9, 1, 7, 40118, 1, 8, 34901, 8, 2, 3, 22, 22, 13, 7, 24, 20, 2, 22, 20, 22, 3, 9, 20, 16, 20, 3, 5, 56, 300, 348, 363, 4104456636, 18, 24, 4, 22, 12, 11, 0, 5, 13, 24, 0, 12, 14, 8, 13, 3, 36, 10, 13, 2143148028, 93327871, 2747443512, 1107115828, 3761351480, 2121127752, 480606446, 3134767433, 1494636995, 4148932980, 2508662982, 867918675, 3522492304, 1881830191, 241244337, 2896003507, 1255279017, 3909128343, 3338902644, 1698575428, 57969698, 2712214607, 1071805922, 12, 17, 9, 10, 56, 489, 391, 405, 3660842071, 1, 10, 31834, 34, 24, 10, 8, 10, 7, 3, 17, 11, 18, 4, 36, 10, 9, 13, 22, 22, 7, 34, 2, 9, 56, 602, 595, 599, 532370683, 14, 15, 3, 12, 12, 4, 23, 7, 56, 348, 448, 457, 2512729757, 56, 353, 483, 487, 1882182507, 26, 13, 18, 5, 17, 21, 10, 15, 55, 5, 17, 23, 27, 16, 8, 21, 55, 56, 481, 585, 596, 3812481091, 23, 5, 18, 8, 13, 11, 1, 1, 32, 14, 11, 8, 13, 6, 8, 56, 433, 451, 457, 548653161, 24, 12, 6, 12, 36, 14, 22, 12, 24, 14, 4, 36, 10, 8, 27, 1, 20, 18, 16, 20, 10, 20, 56, 445, 361, 362, 2857019854, 16, 20, 23, 14, 56, 411, 463, 477, 1839239784, 55, 40, 8, 5, 19, 17, 13, 20, 3, 20, 11, 21, 9, 9, 56, 378, 465, 475, 1251753825, 36, 11, 6, 11, 8, 0, 14, 21, 3595636651, 1955369783, 314975927, 2969284001, 1329061266, 3983177469, 2342570638, 702373770, 3356871552, 1716008589, 75651744, 2730290574, 1089697165, 3744338684, 2103676514, 462881685, 3117061784, 1476661934, 4130995616, 2490459558, 850176679, 3504488097, 1864322013, 223594088, 2878241523, 1237380703, 3892127177, 2251662070, 610807512, 3265696327, 1624967107, 4278747840, 2638356034, 998225372, 3652397684, 2012143345, 371485306, 3025795056, 1385257536, 4039954137, 2399582766, 758862369, 3413634597, 1772773929, 132159011, 2786923037, 1145945832, 3800040055, 2159711734, 519503432, 3173542492, 1533421090, 4187732482, 2547469885, 906680833, 3561244227, 1920585343, 280056331, 2934711929, 1294115834, 3948879418, 2307650141, 667289210, 3321404974, 1681197600, 40744491, 2694847076, 22, 14, 7, 8, 1, 21, 23626, 5, 14, 9, 15, 12, 22, 22, 35, 8, 12, 56, 466, 466, 477, 2022397464, 18, 12, 2, 15, 21, 9, 16, 12, 18, 5, 14, 22, 5, 5, 19]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 14; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 63: + { + // JUMP_243 (specialized) + regs[fp + SLOTS.PC] = 243; + break; + } + case 64: + { + // RETURN_3 (specialized) + var retVal = regs[base + 3]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 65: + { + // LOAD_CONST_6_15_0 (specialized) + var dst = 6; + regs[base + dst] = this._constant(15, 0); + break; + } + case 66: + { + // LOAD_CONST_5_17_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(17, 0); + break; + } + case 67: + { + // LOAD_CONST_4_18_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(18, 0); + break; + } + case 68: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 69: + { + // LOAD_GLOBAL_7_4_0 (specialized) + var dst = 7; + var globalName = this._constant(4, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 70: + { + // LOAD_CONST_8_21_0 (specialized) + var dst = 8; + regs[base + dst] = this._constant(21, 0); + break; + } + case 71: + { + // LOAD_GLOBAL_10_4_0 (specialized) + var dst = 10; + var globalName = this._constant(4, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 72: + { + // GET_PROP_12_10_11 (specialized) + // dst = regs[obj][regs[key]] + var dst = 12; + var obj = regs[base + 10]; + var key = regs[base + 11]; + regs[base + dst] = obj[key]; + break; + } + case 73: + { + // LOAD_GLOBAL_10_4_0 (specialized) + var dst = 10; + var globalName = this._constant(4, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 74: + { + // GET_PROP_13_10_11 (specialized) + // dst = regs[obj][regs[key]] + var dst = 13; + var obj = regs[base + 10]; + var key = regs[base + 11]; + regs[base + dst] = obj[key]; + break; + } + case 75: + { + // SET_PROP_7_8_10 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 7]; + var key = regs[base + 8]; + var val = regs[base + 10]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 76: + { + // RETURN_7 (specialized) + var retVal = regs[base + 7]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 77: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 78: + { + // LOAD_GLOBAL_6_4_0 (specialized) + var dst = 6; + var globalName = this._constant(4, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 79: + { + // SET_PROP_6_7_8 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 6]; + var key = regs[base + 7]; + var val = regs[base + 8]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 80: + { + // GET_PROP_8_6_7 (specialized) + // dst = regs[obj][regs[key]] + var dst = 8; + var obj = regs[base + 6]; + var key = regs[base + 7]; + regs[base + dst] = obj[key]; + break; + } + case 81: + { + // RETURN_2 (specialized) + var retVal = regs[base + 2]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 82: + { + // RETURN_6 (specialized) + var retVal = regs[base + 6]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 83: + { + // LOAD_UPVALUE_0_0 (specialized) + var dst = 0; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 84: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 85: + { + // FINALLY_SETUP_243_2_3_256 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 243, + contReg: 2, + payloadReg: 3, + throwPad: 256 + }); + break; + } + case 86: + { + // TRY_SETUP_209_4 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 209, + exceptionReg: 4 + }); + break; + } + case 87: + { + // LOAD_UPVALUE_7_0 (specialized) + var dst = 7; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 88: + { + // LOAD_INT_2_257 (specialized) + var dst = 2; + regs[base + dst] = 257; + break; + } + case 89: + { + // JUMP_240 (specialized) + regs[fp + SLOTS.PC] = 240; + break; + } + case 90: + { + // LOAD_INT_2_258 (specialized) + var dst = 2; + regs[base + dst] = 258; + break; + } + case 91: + { + // LOAD_INT_2_259 (specialized) + var dst = 2; + regs[base + dst] = 259; + break; + } + case 92: + { + // SET_PROP_7_8_9 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 7]; + var key = regs[base + 8]; + var val = regs[base + 9]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 93: + { + // JUMP_REG_2 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 2]; + break; + } + case 94: + { + // THROW_3 (specialized) + throw regs[base + 3]; + break; + } + case 95: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 96: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 97: + { + // ALIAS_LOAD_CONST_2_1_0 (order: [2,1,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 98: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 61: + { + // ANTI_MOVE_1_10_3_12_13_2_6_8_4_11_5_7_0_9 (order: [1,10,3,12,13,2,6,8,4,11,5,7,0,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[12], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[10], _unsortedOperands[6], _unsortedOperands[11], _unsortedOperands[7], _unsortedOperands[13], _unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[3], _unsortedOperands[4]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[2]]); + // MOVE + var dst = _operands[3]; + regs[base + dst] = regs[base + _operands[4]]; + // MOD + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a % regs[base + _operands[7]]; + // BOR + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a | regs[base + _operands[10]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[11]; + var name = this._constant(_operands[12], _operands[13]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + case 62: + { + // ANTI_UNARY_NEG_7_5_2_4_6_3_0_1 (order: [7,5,2,4,6,3,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[0]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // SHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >> regs[base + _operands[5]]; + // UNARY_NEG + var dst = _operands[6]; + regs[base + dst] = -regs[base + _operands[7]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers.src.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers.src.js new file mode 100644 index 00000000..0d5589c0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/finalizers.src.js @@ -0,0 +1 @@ +function work() { let value = 7; window.SAVED = () => value; window.pauseGate(); return value; } function parent() { try { try { return work(); } catch (error) { return -1; } } finally { window.CLEANED = true; } } window.TEST_OUTPUT = [parent(), window.CLEANED, window.SAVED()]; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/generation.json b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/generation.json new file mode 100644 index 00000000..22e737c9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/generation.json @@ -0,0 +1,108 @@ +{ + "note": "Each encoded file is the VM encoder's output for source.js with the listed options, the leading disassembly comment block removed (the committed corpus format). Math.random was replaced by the seeded PRNG used by cff-loop-return (mulberry32: state += 0x6d2b79f5, then the standard mulberry32 output). Regenerating with the same encoder revision, options and seed reproduces the bytes. A file with a source field was generated from that source with the listed options added to the qualification baseline (target browser, every other representation and timing option false).", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5" + }, + "files": { + "dispatcher-anti-2.encoded.js": { + "options": { + "selfModifying": true, + "dispatcher": true, + "stringConcealing": true, + "antiInstrumentation": true + }, + "seed": 2 + }, + "dispatcher-anti-7.encoded.js": { + "options": { + "selfModifying": true, + "dispatcher": true, + "stringConcealing": true, + "antiInstrumentation": true + }, + "seed": 7 + }, + "cff-anti-2.encoded.js": { + "options": { + "selfModifying": true, + "controlFlowFlattening": true, + "antiInstrumentation": true + }, + "seed": 2 + }, + "arithmetic-alias-specialized-212.encoded.js": { + "source": "arithmetic.src.js", + "options": { + "selfModifying": true, + "aliasedOpcodes": true, + "specializedOpcodes": true + }, + "seed": 212 + }, + "patch-arrays-full-213.encoded.js": { + "source": "patch-arrays.src.js", + "options": { + "selfModifying": true, + "aliasedOpcodes": true, + "macroOpcodes": true, + "specializedOpcodes": true, + "antiInstrumentation": true + }, + "seed": 213 + }, + "finalizers-patch-232.encoded.js": { + "source": "finalizers.src.js", + "options": { + "selfModifying": true, + "aliasedOpcodes": true, + "macroOpcodes": true, + "specializedOpcodes": true, + "antiInstrumentation": true + }, + "seed": 232 + }, + "finalizers-alias-specialized-dispatcher-1.encoded.js": { + "source": "finalizers.src.js", + "options": { + "selfModifying": true, + "specializedOpcodes": true, + "aliasedOpcodes": true, + "dispatcher": true + }, + "seed": 1 + }, + "finalizers-dispatcher-2.encoded.js": { + "source": "finalizers.src.js", + "options": { + "selfModifying": true, + "dispatcher": true + }, + "seed": 2 + }, + "finalizers-dispatcher-1.encoded.js": { + "source": "finalizers.src.js", + "options": { + "selfModifying": true, + "dispatcher": true + }, + "seed": 1 + }, + "finalizers-dispatcher-4.encoded.js": { + "source": "finalizers.src.js", + "options": { + "selfModifying": true, + "dispatcher": true + }, + "seed": 4 + }, + "loops-dispatcher-2.encoded.js": { + "source": "loops.src.js", + "options": { + "selfModifying": true, + "dispatcher": true + }, + "seed": 2 + } + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/loops-dispatcher-2.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/loops-dispatcher-2.encoded.js new file mode 100644 index 00000000..19cac4e2 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/loops-dispatcher-2.encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"values", /* 3 */"sum", /* 4 */"console", /* 5 */"log", /* 6 */"JSON", /* 7 */"stringify", /* 8 */0, /* 9 */1, /* 10 */undefined, /* 11 */16, /* 12 */21, /* 13 */3356898789, /* 14 */"Math", /* 15 */"imul", /* 16 */3153583793, /* 17 */31, /* 18 */409582081, /* 19 */226070299, /* 20 */2725810369, /* 21 */"length", /* 22 */"push", /* 23 */5, /* 24 */10]; +var BYTECODE = [56, 5, 1003, 1068, 2418877890, 8, 1, 17, 20, 56, 890, 902, 903, 3191824374, 26, 2, 6, 10, 14, 9, 23, 2, 32, 11, 0, 55, 5, 18, 16, 21, 7, 1, 6, 32, 13, 24, 56, 1102, 880, 881, 3397930814, 16, 13, 13, 1, 25, 21, 2, 4, 23, 23, 20, 10, 25, 3, 24, 19, 56, 1148, 1152, 1153, 1681299661, 15, 2, 9, 16, 19, 1, 22, 16, 56, 75, 928, 970, 2861813923, 18, 17, 3, 3, 56, 877, 872, 877, 1144362781, 56, 819, 878, 892, 1870995828, 20, 15, 23, 3, 12, 12, 3, 19, 55, 17, 11, 21, 8, 8, 4, 0, 4, 13, 18, 0, 16, 56, 885, 799, 803, 538245800, 55, 43, 39, 284, 24, 20, 3, 20, 23, 23, 24, 7, 56, 132, 970, 1003, 2816895765, 56, 826, 819, 829, 1618789682, 56, 1094, 874, 887, 676717705, 26, 19, 16, 5, 23, 4, 17, 10, 56, 748, 882, 895, 1410980023, 20, 7, 21, 9, 56, 1113, 1092, 1096, 3346518666, 14, 56, 170, 908, 928, 3044495838, 56, 1121, 792, 794, 80945655, 8, 0, 5, 18, 56, 784, 809, 811, 2107129722, 56, 846, 1093, 1109, 903557224, 43, 39, 284, 2, 13, 0, 0, 0, 14, 1, 0, 0, 15, 2, 0, 0, 16, 3, 0, 49, 17, 2, 15, 4, 16, 5, 9, 13, 14, 17, 2, 13, 4, 0, 0, 14, 5, 0, 8, 15, 13, 14, 2, 14, 6, 0, 0, 16, 7, 0, 8, 17, 14, 16, 2, 16, 0, 0, 0, 18, 1, 0, 8, 19, 16, 18, 43, 16, 14, 17, 1, 19, 43, 14, 13, 15, 1, 16, 56, 272, 729, 733, 766248056, 25, 1, 23, 14, 45, 13, 32, 4, 23, 5, 5, 14, 56, 289, 786, 792, 3496742039, 20, 17, 10, 15, 55, 54, 56, 300, 906, 908, 3195138699, 55, 45, 47, 10, 586, 3, 10, 0, 0, 4, 2, 0, 13, 8, 0, 5, 3, 13, 0, 13, 9, 0, 5, 4, 13, 56, 330, 1068, 1072, 2853203043, 36, 5, 5, 8, 5, 5, 13, 22, 13, 5, 0, 34, 11, 13, 33, 11, 11, 1, 7, 1770665023, 1, 12, 3761099685, 12, 12, 12, 7, 13, 12, 12, 11, 11, 7, 7, 12, 1, 8, 8356, 1, 9, 50985, 39, 426, 11, 13, 3, 4, 5, 6, 13, 5, 3, 4, 5, 4, 6, 5, 13, 5, 0, 13, 9, 0, 11, 14, 5, 13, 5, 5, 14, 1, 7, 437768149, 1, 8, 24738, 1, 9, 29207, 39, 426, 36, 2, 16, 27, 23, 13, 9, 45, 3, 0, 13, 10, 0, 45, 13, 42, 7, 10, 3, 7, 8, 9, 58, 7, 4, 3, 0, 4, 11, 0, 19, 5, 1, 4, 0, 4, 11, 0, 21, 6, 1, 4, 17, 4, 5, 6, 0, 5, 8, 0, 21, 6, 4, 5, 5, 1, 6, 18, 4, 1, 0, 0, 5, 8, 0, 21, 6, 4, 5, 5, 1, 6, 0, 4, 12, 0, 21, 5, 1, 4, 18, 4, 1, 5, 0, 5, 8, 0, 21, 6, 4, 5, 5, 1, 6, 0, 4, 13, 0, 11, 5, 1, 4, 0, 4, 8, 0, 21, 6, 5, 4, 5, 1, 6, 35, 4, 1, 0, 5, 8, 0, 21, 6, 4, 5, 5, 1, 6, 2, 4, 14, 0, 0, 5, 15, 0, 8, 6, 4, 5, 0, 5, 16, 0, 43, 7, 4, 6, 2, 1, 5, 0, 4, 8, 0, 21, 5, 7, 4, 5, 1, 5, 45, 1, 0, 4, 10, 0, 45, 4, 56, 801, 874, 890, 2416914181, 4, 4, 0, 6, 11, 0, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 8, 0, 21, 8, 6, 7, 5, 5, 8, 0, 6, 17, 0, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 8, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 18, 0, 12, 7, 0, 6, 0, 6, 8, 0, 21, 8, 7, 6, 5, 0, 8, 18, 6, 0, 5, 0, 7, 8, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 19, 0, 11, 7, 0, 6, 0, 6, 8, 0, 21, 8, 7, 6, 5, 0, 8, 2, 6, 14, 0, 0, 7, 15, 0, 8, 8, 6, 7, 0, 7, 20, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 8, 0, 21, 7, 9, 6, 5, 0, 7, 45, 0, 0, 6, 10, 0, 45, 6, 3420790842, 1780066304, 139603978, 2794248195, 24, 11, 24, 6, 56, 1102, 1141, 1153, 1231110896, 19, 2, 4, 18, 19, 2, 14, 23, 56, 864, 886, 898, 2194950245, 56, 813, 877, 881, 2446133565, 36, 5, 22, 16, 5, 3, 6, 32, 24, 5, 11, 7, 18, 22, 13, 20, 22, 8, 26, 23, 13, 20, 1, 9, 20231, 8, 1856041561, 215705592, 2870420088, 1229700823, 3884339793, 2243741123, 56, 822, 1135, 1149, 1324422306, 35, 4, 11, 12, 13, 20, 11, 19, 3, 23, 2, 26, 16, 9, 18, 13, 16, 8, 7, 56, 1084, 796, 799, 3104813082, 17, 1, 11, 8, 56, 881, 1125, 1130, 3954597906, 22, 9, 5, 4, 56, 1112, 893, 906, 2915529137, 32, 24, 9, 26, 6, 7, 22, 56, 758, 1140, 1151, 3388876845, 25, 1, 16, 17, 32, 13, 4, 12, 22, 14, 18, 56, 1083, 1130, 1145, 447809097, 5, 16, 13, 55, 16, 22, 3, 16, 32, 1, 12, 16, 15, 16, 11, 1, 11, 10600, 55, 25, 5, 3, 9, 12, 10, 12, 16, 4, 14, 12, 11, 15, 19, 56, 811, 809, 812, 72918945, 29, 1554745361, 4209455126, 1403803230, 4058106841, 2417573492, 777276145, 3431650937, 1791458786, 150725214, 2805343919, 1164489647, 3819245496, 2178777766, 537921197, 3192753564, 1551702627, 4205881073, 2565483131, 615616320, 3579529880, 1939261068, 1651996919, 1221135611, 3875929153, 2235439063, 594586689, 3249487044, 1608668213, 4263498805, 2622192833, 981912647, 3635959771, 1995816045, 355282166, 3009504328, 1369372142, 4023684141, 2383354882, 742622253, 3397127219, 1756535821, 115958813, 2770723864, 1129995277, 3783838961, 3201650765, 503208973, 3157511183, 3713201038, 4171424769, 2530749493, 890616890, 3545043995, 1904247808, 263912473, 2918487289, 1277891699, 3932667360, 2291936366, 651088937, 3305206817, 1664994343, 24527922, 2928393214, 1176491437, 3830611364, 2190003417, 549803089, 3204332506, 1563536499, 4218250487, 2577855561, 936906194, 3591871559, 1951073478, 310424626, 2964462666, 1324180698, 3978421322, 2337955281, 697617534, 3351954682, 1711752299, 71028695, 2725671950, 1084810285, 3739528230, 2098871337, 458331177, 3113026586, 1472396317, 4126151805, 2485884154, 845626381, 3499666444, 1859572761, 218985487, 778431627, 3432676819, 1792067037, 151733489, 2806496376, 1165503437, 3820381277, 2179792076, 539061322, 3193050513, 1552778444, 4206955883, 2566558687, 926184516, 3580620020, 1940348017, 299677172, 2954245206, 1313430742, 3968191534, 2327737384, 686873641, 3341757486, 1701028891, 60298262, 2714406116, 1074068605, 3728246768, 2088111132, 447593481, 3101762569, 1461642245, 4115954734, 2475674651, 834880516, 3489414185, 1848822808, 208229488, 2862999038, 1222274144, 3876101146, 2235773961, 595561515, 3249544224, 1609528369, 4263703581, 2623052880, 982658280, 3637359726, 1996575738, 356174972, 3010741461, 1370215507, 4024937923, 2384215108, 743351364, 3397479364, 1757281353, 116786395, 2770895939, 1130839549, 3785075824, 2144855275, 504063069, 3158765525, 1212523741, 3866645357, 2226323434, 586105727, 34, 4, 0, 56, 855, 885, 893, 2249968749, 23, 2, 15, 16, 56, 849, 746, 761, 3871796980, 13, 18, 6, 1, 16, 20, 21, 2, 22, 12, 9, 6, 20, 7, 5, 2, 12, 16, 0, 2, 26, 20, 24, 3, 56, 1143, 806, 811, 536411620, 14, 12, 9, 14, 23, 20, 11, 1, 5, 17, 7, 23, 55, 24, 13, 7, 22, 14, 8, 0, 23, 14, 19, 21, 22, 34, 16, 15, 27, 5, 6, 8, 55, 24, 4, 19, 9, 24]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 20; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/loops.src.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/loops.src.js new file mode 100644 index 00000000..2e6e286a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/loops.src.js @@ -0,0 +1,18 @@ +function fibonacci(n) { + var a = 0, b = 1; + for (var i = 0; i < n; i++) { + var next = a + b; + a = b; + b = next; + } + return a; +} +var inputs = [0, 1, 5, 10]; +var values = [], sum = 0; +for (var j = 0; j < inputs.length; j++) { + var value = fibonacci(inputs[j]); + values.push(value); + sum += value; +} +window.TEST_OUTPUT = { values: values, sum: sum }; +console.log(JSON.stringify(window.TEST_OUTPUT)); diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/patch-arrays-full-213.encoded.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/patch-arrays-full-213.encoded.js new file mode 100644 index 00000000..707733de --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/patch-arrays-full-213.encoded.js @@ -0,0 +1,1262 @@ +var CONSTANTS = [/* 0 */47159, /* 1 */"Efg", /* 2 */57811, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */4, /* 7 */5, /* 8 */6, /* 9 */7, /* 10 */8, /* 11 */9, /* 12 */10, /* 13 */11, /* 14 */12, /* 15 */13, /* 16 */14, /* 17 */15, /* 18 */16, /* 19 */17, /* 20 */18, /* 21 */19, /* 22 */20, /* 23 */21, /* 24 */22, /* 25 */23, /* 26 */24, /* 27 */"window", /* 28 */"TEST_OUTPUT", /* 29 */undefined]; +var BYTECODE = [62, 63, 64, 65, 66, 73, 12, 4, 0, 13, 5, 0, 14, 6, 0, 15, 4, 11, 12, 13, 14, 61, 2, 8, 9, 8, 15, 9, 9, 75, 11, 7, 0, 12, 8, 0, 13, 9, 0, 14, 10, 0, 15, 4, 11, 12, 13, 14, 61, 3, 8, 10, 8, 15, 10, 9, 75, 11, 11, 0, 12, 12, 0, 13, 13, 0, 14, 14, 0, 15, 4, 11, 12, 13, 14, 61, 4, 10, 10, 8, 15, 10, 9, 75, 11, 15, 0, 12, 16, 0, 13, 17, 0, 14, 18, 0, 15, 4, 11, 12, 13, 14, 61, 5, 9, 8, 8, 15, 8, 10, 75, 11, 19, 0, 12, 20, 0, 13, 21, 0, 14, 22, 0, 15, 4, 11, 12, 13, 14, 61, 6, 10, 10, 9, 15, 8, 10, 75, 11, 23, 0, 12, 24, 0, 13, 25, 0, 14, 26, 0, 15, 4, 11, 12, 13, 14, 56, 161, 491, 499, 2510822917, 20, 0, 1, 0, 21, 2, 6, 4, 67, 72, 12, 28, 0, 13, 6, 2, 3, 4, 5, 6, 7, 68, 77, 29, 0, 11, 69, 5, 11, 16, 32, 18, 17, 20, 17, 22, 9, 55, 18, 24, 16, 20, 21, 12, 5, 24, 16, 6, 11, 24, 13, 20, 14, 8, 4, 4, 4, 24, 55, 58, 56, 376, 427, 443, 3622486593, 56, 348, 198, 205, 946055979, 25, 19, 4, 23, 12, 0, 24, 9, 22, 13, 9, 4, 55, 40, 5, 0, 11, 15, 11, 10, 19, 24, 2, 11, 1, 34, 11, 16, 16, 19, 13, 6, 56, 206, 194, 201, 3014926518, 18, 17, 18, 24, 18, 15, 2, 21, 12, 14, 13, 13, 56, 259, 362, 369, 814873824, 18, 2, 21, 11, 34, 18, 8, 13, 0, 3, 18, 36, 9, 13, 56, 247, 290, 298, 3250582767, 24, 22, 7, 24, 21, 14, 15, 10, 56, 209, 260, 265, 56794051, 56, 503, 470, 486, 3870188024, 20, 18, 4, 17, 24, 24, 13, 22, 21, 15, 11, 10, 12, 22, 3, 4, 56, 233, 191, 193, 1571257276, 14, 18, 2, 18, 23, 9, 3, 7, 11, 24, 16, 21, 1, 17, 1434, 4, 3, 12, 2, 8, 1, 27, 24, 8, 7, 21, 18, 14, 12, 56, 205, 519, 533, 2302797477, 23, 5, 2, 22, 12, 10, 0, 0, 56, 429, 364, 371, 2071644857, 4, 15, 56, 513, 421, 434, 3933201418, 16, 14, 20, 2, 56, 520, 205, 219, 1665991461, 11, 17, 6, 21, 25, 1, 8, 16, 36, 13, 5, 17, 11, 16, 5, 18, 19, 2, 6, 56, 347, 348, 355, 3069441311, 18, 23, 2, 18, 56, 204, 514, 521, 719367217, 56, 505, 358, 361, 2034501368, 5, 14, 13, 32, 11, 18, 24, 0, 7, 5, 5, 16, 18, 19, 24, 15, 9, 55, 56, 387, 272, 276, 3657687798, 34, 0, 16, 56, 257, 434, 449, 549782299, 21, 15, 5, 13, 17, 22, 21, 24, 15, 15, 15, 7, 32, 7, 15, 42, 870188700, 3524362921, 1884096946, 243691954, 2897746595, 1257680722, 3911924696, 2271729529, 56, 508, 364, 370, 2521863383, 19, 11, 6, 4, 56, 514, 411, 418, 3457770767, 36, 10, 4, 56, 387, 191, 195, 457127865, 19, 14, 9, 1, 24, 13, 16, 13, 56, 430, 527, 534, 3649440052, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 16; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 62: + { + // LOAD_CONST_8_0_0 (specialized) + var dst = 8; + regs[base + dst] = this._constant(0, 0); + break; + } + case 63: + { + // LOAD_CONST_10_1_0 (specialized) + var dst = 10; + regs[base + dst] = this._constant(1, 0); + break; + } + case 64: + { + // LOAD_CONST_9_2_0 (specialized) + var dst = 9; + regs[base + dst] = this._constant(2, 0); + break; + } + case 65: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 66: + { + // LOAD_CONST_11_3_0 (specialized) + var dst = 11; + regs[base + dst] = this._constant(3, 0); + break; + } + case 67: + { + // LOAD_GLOBAL_11_27_0 (specialized) + var dst = 11; + var globalName = this._constant(27, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 68: + { + // SET_PROP_11_12_13 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 11]; + var key = regs[base + 12]; + var val = regs[base + 13]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 69: + { + // RETURN_11 (specialized) + var retVal = regs[base + 11]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 70: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 71: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 72: + { + // LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 73: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 74: + { + // LOAD_CONST,LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 75: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST,BUILD_ARRAY (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case 76: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 77: + { + // ALIAS_LOAD_CONST_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 61: + { + // ANTI_MOVE_3_6_0_5_4_1_2 (order: [3,6,0,5,4,1,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[1]]; + // GTE + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >= regs[base + _operands[2]]; + // MOVE + var dst = _operands[3]; + regs[base + dst] = regs[base + _operands[4]]; + // MOVE + var dst = _operands[5]; + regs[base + dst] = regs[base + _operands[6]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/patch-arrays.src.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/patch-arrays.src.js new file mode 100644 index 00000000..4fc16690 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/patch-arrays.src.js @@ -0,0 +1 @@ +const a = [1, 2, 3, 4]; const b = [5, 6, 7, 8]; const c = [9, 10, 11, 12]; const d = [13, 14, 15, 16]; const e = [17, 18, 19, 20]; const f = [21, 22, 23, 24]; window.TEST_OUTPUT = [a, b, c, d, e, f]; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/source.js b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/source.js new file mode 100644 index 00000000..10714cdd --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/self-modifying-layers/source.js @@ -0,0 +1,2 @@ +function f(a) { var s = 0; for (var i = 0; i < a; i++) s += i; return s; } +window.TEST_OUTPUT = f(4); diff --git a/test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/encoded.js new file mode 100644 index 00000000..5107e7c5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/encoded.js @@ -0,0 +1,2653 @@ +var CONSTANTS = [/* 0 */1911875161, /* 1 */1875213821, /* 2 */"bX1+DW6HUAN7j0ITSOVtbXfdgTYUS3jfW0tN9UtdDK1zNXWTsA88g7QWT62aPQClmWoK/w5LssYLvfctAOX+YjHvxBMWh7YCE53d9e5q2d7Wq1Y3mto6Tcv1V1X7LsWjzReKksx+yYXoFe++uyqOZ0366E71xftF7k7q0vpn8OOFnlwyrYVGHpyKEHammwEu6rqL1Q9OtcIcVqLTDz6QomEWon1gCluGAjofrngaEeaFbR3Sv0ZEQnzecXJe5nViVqpJNnaafQ5YepcWPapUIkHWSFIN/mdCH9ZUspcpJua7eh2O5xsshoAKG55T5rliDt6psydGtMIxTozxN1qgLvS7ihfnis9+kIkUEoG+MSK6V8Pyzk7Zwe85y67M2rRnwOvpnhMJpYHEHsnyoGfw4+Eu5rLz2RhBrforV7erRD+5qhkRl3nAAtiWmjMJr79iAfmccTLZk0Y1S5XfaWtZ4UMZe7F5OQWDhw9Lc6fmKaFCOUbRQ0s1/0dLNNdnuU8hPJmjcEOPKgN7t2QxTZl74RhpFt/SSzRHudhZcYH5YmGjKGizFgeRgDR5hpEgKZGgBSujVzX7xEDA2esx76jn4K93CfOxmDIBhYnpEMbo3Wf726swt7j3wdBJ0vA8aIqj0zjzkMcJ0YHaCJ+Q6TsHoNpYIvCRSTLAg0g7MJ7f+2AM6PARPoi/AD6YlO85eLngeKgeMGDIfEAA+G5QtCgdoKspApBjeFyAYBh6sHMoO2B06GxwM9ihQz1ISNBgaCPgbJh+EGG4TgBAiBZwuek9II24KNCnVwPAoEh/0Ck4gaAyGPt3TQixgHcI57E8GJrg9mfS0+9ItUCX2Q9QuvhDYPmrpDDciLEB6HnWEMWYsyPrr6xQEfnyQbnY1rDyK//fzGjT8PAZI4GkODOTuO8OY+3YBlHxOYfAKkuh/3FrmCAZucYRFpmxAH6PGhN9t1Qhdlko8Z5IT9+yS0K3aNleYVX5e5FAD0qDawddk1l5teEZKXG/RctFR2D7U1F12VYhv64rE6N3AQPEnj4x5akHIWLrjXcv275ONzmy0RhphOIAZ6qTxT7PgsUJ03Hh6gqnmztVr5ZaqcHoSfnS3LaWI/af1XLY9uMBuInJNiea7e/3etvWw0nzQf7G9VKG/1piqSYYsp4JM4aVehiO8RvwpmoqslEf9pNCG963M2G26CJjbg/2U5pIDlC6UBaN6gd+oelHUmy+ZsJHRhfyRm4yomU6cK5FGqeHRwpjnnYqY6IOXl/yUXY0wtFOCDK81R5ulvoNFpOLIj5ZiqIVIH7i4iumszMizr5CH8aGTeDahabIK8+f6goeho4dU7KANr2jyO/rcurW5E3FvbHW9Gq+/wprvBb1vdIN34bBctLv9yPGpiktnl0zxbFKRt+EKzSnhBqIbXX1tYJwD7yzexfJ4hV9stUXVR6/S8sJV2b7UW15pVs1mI4rA4CHb/NPnUQlfKVpXXHLLUdn2z1PMjWVpV9tZvN9F3CDTT9RlREVKn2t7BNXmjszz6tEDvX/bVTdPKfwMwSfkAQDhYodOK2ZJDRb5e/vdPTE0E3vvabUMmPI/0CT9xTqvfkN2pT4dOXvzVuqtOTMsl0UxMR089/pI9CkwxzJje8F64QsDKK7PiS/7Dh8qNU+RPg82tMhVIT8KWyClQE0woxAHOd3MOxZnGwkVrRHXHLMjkc+3J+sGiQrpEtsc/w1BASMaTxplFXkUHyH7CpUIDxrzC9UEPRybEgcUrSEMxGckARK9JAdeayEJHBUqM/xRDfElk02vKYkEGSX/zaU5AQFvNENi5TwZJbvrluxtF3Mul0A9OB09N/ZI9G0zxy7jeEFtJSmLMq7ESfh7OZ978XPRNU8jNPJZ+n8EW2jlScFl4wzG+l3DuuDXLA1dLWlXDXDiHcr05qsBSXulUUNXPteBxmLfj8rpbjlAH21zENHLMNyz1FVDeV3bUUdVLdPC3Kfkgoe9V8dfK1NN05TZM9LQ2TV7y0VvbcmT2ue/zeLswYOvW4touZ7Y+vvC1OCxjvNgF0Y9eJq3p+UK8C24BqVjf/1jpIFL6Sj11bU0rR9", /* 3 */"M4V0BTKNdHO85zgjXr1n1X1FX/0=", /* 4 */"VTVJXQ3jincWE4OM", /* 5 */-750369459, /* 6 */846082073, /* 7 */undefined, /* 8 */"5QQaj5IbD7emLFVdbPVrdQ==", /* 9 */1397793552, /* 10 */1121916860, /* 11 */-2059858219, /* 12 */787667294, /* 13 */"oidDosou6ZGqec6Ouxrjt+Er", /* 14 */"HJ4VdoPiKF6Gszc2", /* 15 */-1260271070, /* 16 */"NfiFETWYugA=", /* 17 */"LwSV/AYMlJU=", /* 18 */127718144, /* 19 */"R/1eTUXVIyNZr0sTv3Y=", /* 20 */"XXJHjnYCooYLMq6d", /* 21 */"X5lXAVK5XxFSaw==", /* 22 */"9Ivn/vOKzxHiucwi8Ob+cxPOi0I=", /* 23 */291353206, /* 24 */"7kh98fBZG6HzOCOXsAsOf2IJRZE=", /* 25 */"WaYTann+J0poxg==", /* 26 */"UisD5Yh9Gs2LRiHD", /* 27 */1359309385, /* 28 */"8vvuR/jI", /* 29 */"/j1jhOQUC/zda46k", /* 30 */"ORyQc330", /* 31 */"6GTPr8czw0TqzNN99tX5JA==", /* 32 */1758530454, /* 33 */"DV1bJXKllBw=", /* 34 */"JT2Cgl0XUPNwb0SlFTUWTQ==", /* 35 */"58Kuee/RwarGPg==", /* 36 */"mYcOErj+CmKRpQ==", /* 37 */399226048, /* 38 */"pbb5E9uP", /* 39 */"3irqptLbk0+X+vhG3cktMZmmPuM=", /* 40 */"HsbLSka+XSpaZlz6WI5fBkeCdw4=", /* 41 */"/GHj6tWW3jvWj+Qa8vbmaQ==", /* 42 */242041525, /* 43 */"tObuY+Xv95LnBtyNyh3K9g==", /* 44 */"XZ5x9utyHx6rgx82nJIcbo3l", /* 45 */"uzodzmlaf/ZwanXeVaY=", /* 46 */"rezxY+Ln96w=", /* 47 */-2045703627, /* 48 */"ZZEoKVKjqNckU9b4X0E=", /* 49 */"7NDwOdNB/Nj8cA==", /* 50 */"7y/W2+pw9MjsQe3JkbAcXw==", /* 51 */"n6cP0rROJcKvRSHO", /* 52 */-831788676, /* 53 */"XQXphWcdHbNNJ1uj", /* 54 */"C8O7uMMx", /* 55 */"c6S4ZTb8pAw2hKgDKoykFA==", /* 56 */176126344, /* 57 */"uMvRvvsq5+XSfeKS3RY=", /* 58 */"jxkitqYqAd6lawL22Vr3wQ==", /* 59 */"SCpKvmMK", /* 60 */660053720, /* 61 */"SKWKFReNs3x1h2gL", /* 62 */"+YnjAeaIyTDVn4Jjx+jwUKTJ", /* 63 */"QAFZeQEP", /* 64 */"JJJpNnyCUBpa9npq", /* 65 */1553341656, /* 66 */"8XXm7Mxc", /* 67 */"g8AsSbnxOFmdoDgn+5N3D3CBuRk=", /* 68 */1772083038, /* 69 */"eB5qtrgiMl488g==", /* 70 */"I8WGfR3N2lY=", /* 71 */"mJQND617zxQ=", /* 72 */729290534, /* 73 */"VX5K6lAmTr4j0mxGocMGTqbS", /* 74 */"yGj2IJm53VHY2A==", /* 75 */"NWK+niky", /* 76 */-770413395, /* 77 */"8oHo+Z+WkSo=", /* 78 */"As23RBvzkG83o50kHK2mHYeE", /* 79 */1730895450, /* 80 */"geYVK5C/DQu1hh/5wpGKJhij", /* 81 */"FCsd321L", /* 82 */-640377655, /* 83 */"oJw5LFOkStw=", /* 84 */"gXBmmb8hG7my0CNLm/8kSw==", /* 85 */"/L61ysxXyPs=", /* 86 */896724390, /* 87 */"/sdow5pY8/Hdacfg", /* 88 */"UBWGug4v0Nsxd6/KJV2XNX61", /* 89 */"sptZN/GM4R0=", /* 90 */-2144664776, /* 91 */"BeehYz6fvTIrhasdqYop97qb", /* 92 */"k5b94p55FNbqSgk+u8tGVor6", /* 93 */"MLCoCCmApAc=", /* 94 */1618855927, /* 95 */"9Qjmh977z5A=", /* 96 */"c9xbTFXEE1S4rwkku6Q87Jl9GQQ=", /* 97 */-796470838, /* 98 */"osI+SUqx/No=", /* 99 */"FbTL3BRsp+fEnOYMzIXkBeeM", /* 100 */"PyuNZzf7lo40BTWFnw4=", /* 101 */-1130597290, /* 102 */"cs62WicmrKl5Enn2", /* 103 */"B92ONE9LC9+9dD71o10UrQ==", /* 104 */"fstONU5dCO2KZBsTm48=", /* 105 */349368936, /* 106 */"AoaoAQS6sRYV6g==", /* 107 */"0pf0a5j+Emr51cg+9ULGx8Nz0u4=", /* 108 */-1652088496, /* 109 */"EkKbtifS", /* 110 */"OLRrHFlka/RcXK7NZMRoVA==", /* 111 */"ik8HMEfYQGAl+EIQeIg9AHSI", /* 112 */"B06f2gemsyscnicK9YF0Hg==", /* 113 */"tqQ9JLnvXnQ=", /* 114 */"Z75lxulKbP4=", /* 115 */"YkpkxjV6", /* 116 */"hAUGjrMa", /* 117 */"LqOfLuFV1vXwTj7bmzc7q6LeLGU=", /* 118 */"JL9ZKXBh", /* 119 */"3mfE0DNJvUEt2cxQ", /* 120 */"VcVPTTXdZLc=", /* 121 */"ZTiksB9Y", /* 122 */"MyGE2Qtxq/gmQ+vPArM=", /* 123 */"b6hWMGiYbwB++A==", /* 124 */"qGJy/riTcRbKgn8O", /* 125 */"48HPeNvQuS8hoA==", /* 126 */true, /* 127 */"1nub56OqKjE=", /* 128 */1972473230, /* 129 */895866529, /* 130 */1328854597, /* 131 */-1905610689, /* 132 */"FC5AonUWaZJDfhiGhwpPvpIr", /* 133 */"aDKzxjxCYv52UnQmS6I=", /* 134 */-543915720, /* 135 */"U41FPWynTmtj/zxLN8VEvQ==", /* 136 */-886525963, /* 137 */"f/NCn3EDQYkVEXjpjmYZ2w==", /* 138 */"xpGaCfaA8hg=", /* 139 */-115574447, /* 140 */"G+ILbk7Sf7YmQmPe", /* 141 */"2Qm/geca", /* 142 */-504841522, /* 143 */"aj+LpCgVl/0rDYqERTt8rw==", /* 144 */"Q8a2QgY+v9MeZmbiQG4=", /* 145 */"2BDzuJQD", /* 146 */1707533160, /* 147 */"WB1i9VdlcN9TKw==", /* 148 */"JkZR+kNO", /* 149 */-1182405398, /* 150 */"L1e1yBrBj0kV0c14lePJlw==", /* 151 */"P5nOYHfr", /* 152 */"ai5KmnvmRmpg3g==", /* 153 */2050141426, /* 154 */"A1+IozU3kJw0DaeFZRVtr3s7bac=", /* 155 */"FuC+KBGwVxg=", /* 156 */"uosvfJmE", /* 157 */267512142, /* 158 */"gw5TcmgaW65EOmvW", /* 159 */"6Ubpyxh+gOo7JoC5yRLrluMD3o8=", /* 160 */"55EReYTg/1vLt4rIL0E=", /* 161 */-1628262863, /* 162 */"0E8twtFGHDI=", /* 163 */"629sG620AQ2znSx1pPQqHw==", /* 164 */"EQPbhnENU70=", /* 165 */-438566397, /* 166 */"poIoHpWzGCayog==", /* 167 */"mDs6zm9afcZ9TlvSAKY=", /* 168 */-6110539, /* 169 */"Of+yE1eX1QI=", /* 170 */"/GiJ8OtJ88HVOANTldcNYw==", /* 171 */"0x29ih/2vJs=", /* 172 */939397520, /* 173 */"jWvnn+sq", /* 174 */"M/ykawXULLw=", /* 175 */"QKWXNS5Ng9wPR1PzT18=", /* 176 */-1993737681, /* 177 */"YRh0YIP5B1CqyCZA", /* 178 */"dRNjaU7hU1lk0Su7jEckyw==", /* 179 */1102078678, /* 180 */"8R3dtD4rld9uaw==", /* 181 */"GiB5mEkQWPi4BzqInjALqIHh", /* 182 */"U2ZNKnG+ako=", /* 183 */-64475748, /* 184 */"v+8Xe+nW20n6QcLG/VI=", /* 185 */"q1A1+Zph", /* 186 */-2131873532, /* 187 */"jFsQ7oR6PObCqcsyxobEAw==", /* 188 */"kSoJvqorOZaPegaOggU=", /* 189 */"RKFTKU+fUAsxh5IaRbFGOWWhXO8=", /* 190 */-1392907789, /* 191 */"B/ejajYh", /* 192 */"UocKDaid", /* 193 */"j1tO96dqT6F7KXWx", /* 194 */1242271443, /* 195 */"2F36xNh0", /* 196 */"VvVcTVPDbzdNsw==", /* 197 */"ajihUCLIp38=", /* 198 */"f/AfmBsA", /* 199 */"kEEW+JVQG9idI8yoxBDHeewB7Jg=", /* 200 */"4TDuiLMDXv+7EAaphCEHqe/Qw3s=", /* 201 */-1179556797, /* 202 */"IWKc/SsWs4ofPr+L8Rbyeg==", /* 203 */"Leq1dgbahUFeRg==", /* 204 */"HQbGmjbu7Xs=", /* 205 */"HqzoPdvE", /* 206 */"2zDLmLThu2mJ0NtI7kf72w==", /* 207 */"", /* 208 */"0p7HK/OnutpWbg==", /* 209 */"zO3LXf3Ew8T/X+Pz", /* 210 */"eaJY2kduUPpxRn/KfT57qrrha35rEmeW", /* 211 */"QkFmyRl3Ldssrw==", /* 212 */"tEsC9pZaJq4=", /* 213 */"QOhEcEjYTECsuALQ", /* 214 */"D8D5SzbQUjhZoB0YknBmCJyD", /* 215 */"cbltUz3XKEMkzzFRyTkDoY4a", /* 216 */"wizUvMoX2IM=", /* 217 */"IaaHPTLSoVYmwvxP", /* 218 */"pPJmGbKhDCmZXg==", /* 219 */"cnN7zzpBrTlXsbNY", /* 220 */"8v0eZq+qYT4=", /* 221 */-370880250, /* 222 */-611792106, /* 223 */1706416729, /* 224 */"mn8A05JW", /* 225 */"PTmBxiNL", /* 226 */-752482671, /* 227 */"WO5vIkK2", /* 228 */1263007327, /* 229 */"D5uQdjDpqSEjqeEy6F8Hw792CtE=", /* 230 */"ZVtxr2E7", /* 231 */122321268, /* 232 */"IsCySQrQkqgiIIefXnBxiA==", /* 233 */"fkKrSTbeimp95naa", /* 234 */"9T6j0RdK1P4DSg==", /* 235 */1022693591, /* 236 */"nr0SDZGWMvuqbzjb", /* 237 */"L6I6FqJyMQ5+hjY6ca44Gg==", /* 238 */"KUa22zVu", /* 239 */-1799805922, /* 240 */"FJvfDwa7", /* 241 */"pETZ9PpvNSOLpOQt7521BA==", /* 242 */-1636494404, /* 243 */"G1a8MS3KZkZm+m1u", /* 244 */"wKAYG5ePa/slgXwJCbF0KWvjvX8=", /* 245 */-1885603026, /* 246 */"3EHlyv9e3SszpssaIu7WeTuC", /* 247 */"xA7xgv0ZKq7SOi6mpOsofonK6kE=", /* 248 */"t0IwxoVxEOpuJkC6", /* 249 */556333827, /* 250 */"5RbAitv9hYXmEg==", /* 251 */"Am3E9cVS1M7Vw8RX", /* 252 */-569232044, /* 253 */"pA4OlZ4lnq7OWvj3mkur3vBK", /* 254 */"ARr1dvwL3J/cCtO2tR357fdm3tI=", /* 255 */782611095, /* 256 */"6Bum9Pps2p3RJc+0/dzWQzLEplw=", /* 257 */"NVmXwiPHgFNm73FhYpk=", /* 258 */"VaDSKMtR9fnsQILY", /* 259 */2104009327, /* 260 */"+5D3ONah/1nf8PlI8t8PSw==", /* 261 */"n9bBeuzB7Fk=", /* 262 */177378977, /* 263 */"Y4Z4/j+SVxZeog==", /* 264 */"mbUSWp7uDHr3lw8KkL4BCuGV", /* 265 */"oNlGcWT3aUM=", /* 266 */753928154, /* 267 */"lPMFdqrCS052tkna", /* 268 */"6DsKoIfY", /* 269 */"ruByaFTwYhhxgEM4SpA=", /* 270 */-452911145, /* 271 */"bdVXpUs9dZOuFySDuQw=", /* 272 */"ewtbhU39S21CJQGj", /* 273 */-1522079543, /* 274 */"UGsO310rULFoGVOB", /* 275 */"q51OLYW2", /* 276 */862219611, /* 277 */"XkJkzrgyTVY=", /* 278 */"uhxrjV71RGVOL2K7M1cmy2B9", /* 279 */"oJchEt++2wWeldZ+0erGJx66", /* 280 */1211893930, /* 281 */"pIQpfeqNHBVOu2IvSltL929Nvd0=", /* 282 */"NYOCfAaVbCU=", /* 283 */2142902507, /* 284 */"yNPFTu1C", /* 285 */"20vE8PRp0qHbOMaQ9Qc=", /* 286 */"+nbhkqYp7qHX2uVOvPvnR8bKFzE=", /* 287 */-1607424304, /* 288 */"CNOSbFHl0WULLYy8", /* 289 */"xiYLmmUOf3o1Bk2KSTI6pg==", /* 290 */"2s4cWszxzm7jIvS33DM=", /* 291 */1939411938, /* 292 */"S0oS/qpK", /* 293 */"sUU4xexcAzOCp0kjm+xQdQ==", /* 294 */"dzL9qSbmWXqO7w==", /* 295 */"5/DAWPrBTrk=", /* 296 */"clFRqRch", /* 297 */"1NHcucwuwZo=", /* 298 */"wSLvvukT6IfsAt6OmRU=", /* 299 */"obMqH59iBvW7XT7NysYRW5Hv", /* 300 */"YXZa6kUeSao=", /* 301 */"3xozj4d7NOefKlm9v9UyRYf+81M=", /* 302 */"5wvlnNv0", /* 303 */"18wQdZPVFa3TPA==", /* 304 */-1288835364, /* 305 */-48714099, /* 306 */"Bcb+uzUu49rCYQ==", /* 307 */46427558, /* 308 */"ehTtlAk8nKMQ1Ih8Zsy4RWO0", /* 309 */"oIheAF24phA6mJ9jFuiXUA==", /* 310 */-1767865191, /* 311 */"OaywPCWkoWsp/KBELsT9vQ==", /* 312 */"+YuL98GaxC6xuTtSyc4+Qg==", /* 313 */-2115219255, /* 314 */"XArnhv/9", /* 315 */"IB9UsV0JVYE=", /* 316 */"GL+TE2iXKuE=", /* 317 */"r5QuFLV9Kew=", /* 318 */1037012852, /* 319 */"IpHqCuWHkRv8jsR6q+E+KbOy", /* 320 */1383166890, /* 321 */"cO4oGpe3", /* 322 */"Q5qs5jZqp9EMtpNC", /* 323 */-440286008, /* 324 */374473995, /* 325 */1278262575, /* 326 */"CDKb3ixivfdhEkaOXwJhmg==", /* 327 */"CPi2cNUb8Kc=", /* 328 */-880515078, /* 329 */"JCq65gV6p49TGmWGdQpmkkLm", /* 330 */732592647, /* 331 */"kqYYMiWa", /* 332 */"TLAiSLzBPHiv8BFYeyBtqA==", /* 333 */"rWY549dfJTMXxWRd", /* 334 */-417148009, /* 335 */"t/MGj24BZblWMVGZW2c=", /* 336 */-294722561, /* 337 */"zsX8VMI826vkZw==", /* 338 */704958721, /* 339 */"YPAAWKnBSchyUA==", /* 340 */-1091487179, /* 341 */-1312652595, /* 342 */"frNFJU9t", /* 343 */1470706950, /* 344 */"ABvLjzl7mIY4Da29DCVjYw==", /* 345 */-1477613126, /* 346 */"JWKo5zNSsL4Twk5KFf4Eag==", /* 347 */-490471420, /* 348 */"tPMdd77irak=", /* 349 */"INu+TAC1niV2bTTz", /* 350 */-481387214, /* 351 */"YChLoDgoepBc+GCAGAiwsVgoUOA=", /* 352 */"2vy8ZwCs2Tw3lM8F63w=", /* 353 */698906409, /* 354 */"md4uQ7JHANOzbinhrBk="]; +var BYTECODE = [83, 84, 47, 6, 4430, 1, 83, 0, 0, 85, 42, 5, 6, 1, 7, 47, 8, 7517, 4, 87, 0, 0, 86, 47, 14, 105, 1, 107, 1, 0, 1, 5, 61, 3183141085, 13, 13, 2, 14, 3, 12, 87, 88, 89, 145, 11, 11, 3, 8, 4, 5, 9, 10, 11, 90, 62, 13, 52975, 16, 13, 12, 15, 13, 42, 15, 2, 1, 16, 148, 6, 16, 846082077, 42, 17, 2, 1, 16, 91, 116, 9, 501063321, 10, 124, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 92, 48, 16, 3, 15, 17, 18, 93, 148, 7, 14, 0, 94, 119, 102, 8, 3664201321, 103, 9, 1397780325, 105, 10, 1121916860, 104, 11, 2235123422, 47, 98, 10552, 3, 15, 0, 0, 95, 148, 12, 20, 787652194, 61, 296738579, 103, 103, 21, 20, 13, 102, 148, 10, 22, 1121916860, 61, 2596854902, 102, 103, 23, 22, 14, 103, 148, 15, 24, 3034646441, 63, 102, 24, 102, 21, 163, 25, 102, 104, 64, 103, 103, 103, 103, 102, 103, 103, 100, 102, 25, 103, 102, 103, 103, 65, 103, 102, 103, 102, 103, 102, 102, 103, 103, 100, 100, 103, 103, 103, 62, 102, 14141, 100, 102, 102, 100, 102, 115, 95, 1064236996, 101, 4153846215, 66, 103, 103, 102, 102, 101, 102, 102, 3681729390, 103, 102, 103, 95, 103, 16, 101, 67, 100, 103, 103, 0, 103, 101, 103, 101, 102, 103, 103, 1, 68, 95, 103, 95, 102, 103, 2072268796, 103, 103, 103, 101, 17, 103, 115, 96, 28987, 97, 26081, 77, 148, 18, 26, 127689025, 61, 3181095413, 102, 102, 23, 26, 19, 102, 69, 102, 102, 103, 102, 103, 21, 102, 103, 23, 103, 103, 27, 64, 103, 102, 103, 103, 103, 103, 102, 100, 103, 27, 103, 102, 102, 103, 65, 103, 102, 102, 103, 102, 103, 103, 103, 102, 100, 100, 102, 102, 103, 115, 95, 555228339, 101, 1714165556, 66, 103, 103, 103, 103, 101, 102, 102, 456474520, 103, 102, 103, 95, 102, 20, 101, 70, 103, 100, 103, 102, 101, 103, 102, 102, 145241642, 102, 21, 101, 68, 95, 102, 95, 102, 102, 3013512059, 102, 102, 102, 101, 22, 103, 115, 96, 38911, 97, 107, 77, 116, 95, 3132419137, 96, 27531, 97, 51863, 77, 148, 23, 28, 291344055, 68, 29, 103, 23, 103, 103, 2072268796, 103, 102, 103, 28, 17, 103, 61, 4266013051, 103, 103, 23, 29, 24, 103, 69, 102, 103, 103, 103, 103, 21, 103, 102, 23, 102, 103, 30, 64, 103, 103, 103, 103, 102, 103, 103, 100, 102, 30, 102, 102, 102, 102, 65, 102, 102, 102, 102, 103, 102, 102, 102, 103, 100, 100, 102, 102, 102, 62, 103, 55466, 100, 103, 103, 100, 103, 115, 95, 327038415, 101, 1485968538, 66, 102, 102, 102, 102, 101, 103, 102, 3392760779, 102, 103, 103, 95, 102, 25, 101, 67, 100, 102, 103, 0, 102, 101, 102, 101, 102, 102, 102, 1, 68, 95, 102, 95, 102, 103, 1523345737, 102, 102, 103, 101, 26, 102, 115, 96, 40465, 97, 1954, 77, 116, 95, 2169159075, 96, 53945, 97, 28698, 77, 148, 27, 31, 1359324356, 66, 103, 103, 103, 102, 32, 102, 102, 3053576369, 102, 102, 102, 31, 103, 28, 23, 61, 3186385692, 103, 102, 23, 32, 29, 103, 69, 102, 103, 103, 102, 103, 21, 103, 103, 23, 102, 102, 33, 64, 103, 102, 103, 102, 102, 102, 103, 100, 102, 33, 103, 102, 102, 102, 65, 102, 102, 102, 103, 103, 102, 103, 103, 102, 100, 100, 103, 103, 103, 62, 102, 5074, 100, 102, 103, 100, 103, 115, 95, 1801932759, 101, 234235666, 66, 103, 103, 103, 102, 101, 103, 103, 1166706848, 103, 103, 102, 95, 103, 30, 101, 67, 100, 102, 103, 1, 103, 101, 103, 101, 103, 103, 102, 0, 68, 95, 102, 95, 102, 103, 2245839489, 103, 102, 102, 101, 31, 103, 115, 96, 3324, 97, 1071, 77, 116, 95, 962643897, 96, 27480, 97, 51252, 77, 148, 32, 34, 1758528717, 66, 102, 102, 103, 102, 35, 102, 102, 40361998, 102, 102, 103, 34, 102, 33, 23, 61, 3459376260, 103, 102, 23, 35, 34, 102, 69, 103, 102, 103, 102, 103, 21, 103, 102, 23, 102, 103, 36, 64, 102, 103, 103, 102, 103, 102, 103, 100, 103, 36, 103, 102, 103, 102, 65, 103, 102, 102, 102, 103, 102, 103, 103, 103, 100, 100, 103, 102, 102, 115, 95, 3644385126, 101, 508371801, 66, 103, 102, 103, 103, 101, 102, 102, 1728999168, 102, 102, 102, 95, 103, 35, 101, 70, 102, 101, 102, 102, 101, 103, 102, 103, 2245839489, 102, 31, 100, 68, 95, 103, 95, 103, 102, 1273912613, 103, 103, 102, 101, 36, 102, 115, 96, 30283, 97, 65066, 77, 116, 95, 3769591165, 96, 18984, 97, 20218, 77, 148, 37, 37, 399193905, 68, 38, 102, 23, 102, 103, 3064829962, 103, 103, 102, 37, 38, 103, 61, 1426602418, 103, 103, 23, 38, 39, 102, 69, 102, 102, 102, 103, 103, 21, 102, 103, 23, 102, 103, 39, 64, 102, 103, 102, 103, 103, 103, 102, 100, 103, 39, 102, 103, 103, 102, 65, 102, 103, 103, 103, 103, 103, 102, 102, 102, 100, 100, 102, 103, 103, 62, 102, 15852, 100, 102, 103, 100, 102, 115, 95, 2157027074, 101, 3315984289, 66, 103, 103, 102, 103, 101, 103, 103, 474061830, 102, 102, 102, 95, 102, 40, 101, 67, 101, 103, 103, 2, 103, 101, 103, 100, 103, 103, 103, 1, 68, 95, 102, 95, 103, 103, 2211313669, 103, 103, 103, 101, 41, 103, 115, 96, 18434, 97, 47700, 77, 116, 95, 2239071125, 96, 5057, 97, 55127, 77, 148, 42, 40, 242047739, 68, 41, 102, 23, 103, 103, 3316678665, 103, 103, 102, 40, 43, 103, 61, 1971881601, 102, 102, 23, 41, 44, 103, 69, 103, 102, 103, 103, 103, 21, 102, 102, 23, 103, 102, 42, 64, 103, 102, 102, 103, 102, 102, 102, 100, 102, 42, 102, 102, 102, 103, 65, 103, 103, 103, 102, 103, 102, 102, 102, 102, 100, 100, 102, 103, 102, 115, 95, 3695289845, 101, 559275478, 66, 103, 103, 103, 103, 101, 103, 103, 1790182352, 102, 103, 103, 95, 103, 45, 101, 70, 102, 101, 103, 102, 101, 103, 103, 103, 3583047915, 103, 46, 100, 68, 95, 103, 95, 102, 102, 2072268796, 102, 102, 102, 101, 17, 103, 115, 96, 1014, 97, 58836, 77, 116, 95, 1763174246, 96, 41249, 97, 14303, 77, 148, 47, 43, 2249294182, 66, 103, 103, 103, 102, 44, 102, 103, 387236245, 102, 103, 102, 43, 103, 48, 23, 61, 880528228, 103, 103, 23, 44, 49, 103, 69, 102, 103, 103, 103, 103, 21, 103, 103, 23, 103, 103, 45, 64, 103, 102, 103, 102, 102, 103, 102, 100, 102, 45, 103, 103, 102, 103, 65, 103, 103, 103, 103, 102, 103, 103, 103, 103, 100, 100, 103, 102, 102, 62, 102, 49815, 100, 102, 103, 100, 103, 115, 95, 1054655792, 101, 2213614253, 66, 103, 102, 102, 102, 101, 102, 102, 3402952239, 103, 103, 102, 95, 102, 50, 101, 67, 101, 102, 102, 0, 102, 101, 102, 100, 102, 102, 102, 1, 68, 95, 103, 95, 103, 102, 854373684, 103, 102, 103, 101, 51, 103, 115, 96, 33006, 97, 60671, 77, 116, 95, 653747738, 96, 29667, 97, 2328, 77, 148, 52, 46, 3463150747, 68, 47, 103, 23, 102, 103, 1273912613, 102, 103, 103, 46, 36, 103, 61, 1656523028, 102, 103, 23, 47, 53, 102, 69, 103, 103, 103, 103, 103, 21, 103, 103, 23, 103, 103, 48, 64, 103, 102, 103, 102, 103, 103, 102, 100, 103, 48, 102, 103, 103, 103, 65, 102, 103, 102, 103, 103, 103, 102, 103, 103, 100, 100, 102, 102, 102, 115, 95, 803045072, 101, 1962000333, 66, 103, 103, 102, 103, 101, 103, 103, 1669944224, 103, 103, 103, 95, 103, 54, 101, 70, 103, 100, 103, 103, 101, 102, 102, 103, 1857505674, 102, 55, 101, 68, 95, 103, 95, 102, 103, 1273912613, 102, 103, 103, 101, 36, 103, 115, 96, 31911, 97, 14309, 77, 116, 95, 1214600921, 96, 2585, 97, 33239, 77, 148, 56, 49, 176154393, 66, 103, 103, 103, 102, 50, 102, 103, 581915397, 103, 103, 103, 49, 103, 57, 23, 61, 520911407, 102, 103, 23, 50, 58, 103, 69, 103, 102, 102, 103, 102, 21, 103, 103, 23, 103, 102, 51, 64, 102, 103, 102, 102, 103, 102, 103, 100, 102, 51, 102, 102, 102, 103, 65, 103, 102, 102, 102, 103, 103, 102, 102, 102, 100, 100, 102, 102, 102, 115, 95, 3010601775, 101, 4169558452, 66, 103, 103, 103, 102, 101, 102, 102, 280814593, 102, 102, 102, 95, 102, 59, 101, 70, 102, 100, 103, 103, 101, 102, 102, 102, 2245839489, 102, 31, 101, 68, 95, 102, 95, 102, 103, 3064829962, 103, 102, 102, 101, 38, 102, 115, 96, 48073, 97, 12102, 77, 116, 95, 1036623268, 96, 11853, 97, 26387, 77, 148, 60, 52, 660049223, 68, 53, 102, 23, 102, 102, 2671826207, 103, 102, 103, 52, 61, 103, 61, 293043007, 102, 103, 23, 53, 62, 102, 69, 103, 103, 103, 102, 102, 21, 102, 102, 23, 102, 102, 54, 64, 102, 103, 102, 102, 103, 103, 102, 100, 103, 54, 103, 102, 102, 102, 65, 102, 103, 103, 103, 102, 103, 102, 103, 102, 100, 100, 102, 103, 102, 62, 103, 41113, 100, 103, 103, 100, 103, 115, 95, 3015090244, 101, 4174046273, 66, 102, 103, 103, 103, 101, 103, 102, 3010728632, 103, 103, 103, 95, 103, 63, 101, 67, 100, 103, 103, 0, 103, 101, 102, 101, 103, 103, 102, 0, 68, 95, 102, 95, 103, 102, 2157832968, 102, 102, 103, 101, 64, 102, 115, 96, 56350, 97, 47686, 77, 116, 95, 3104392827, 96, 986, 97, 15031, 77, 148, 65, 55, 1553373041, 68, 56, 103, 23, 103, 102, 2072268796, 103, 102, 103, 55, 17, 102, 61, 3579735897, 102, 102, 23, 56, 66, 103, 69, 103, 103, 103, 102, 102, 21, 103, 103, 23, 103, 103, 57, 64, 102, 102, 103, 103, 103, 103, 103, 100, 102, 57, 102, 103, 102, 103, 65, 102, 102, 102, 102, 103, 102, 102, 102, 103, 100, 100, 102, 102, 103, 62, 103, 32623, 100, 102, 102, 100, 102, 115, 95, 2545598106, 101, 3704552397, 66, 103, 102, 103, 102, 101, 103, 102, 3485742462, 102, 102, 102, 95, 102, 67, 101, 67, 101, 102, 102, 2, 103, 101, 102, 100, 102, 103, 102, 2, 68, 95, 103, 95, 102, 102, 2211313669, 102, 102, 103, 101, 41, 103, 115, 96, 48388, 97, 10483, 77, 116, 95, 3128213289, 96, 42007, 97, 55440, 77, 148, 68, 58, 1772067113, 66, 102, 103, 103, 102, 59, 102, 102, 2529770592, 103, 103, 103, 58, 102, 69, 23, 61, 4152618755, 102, 102, 23, 59, 70, 102, 69, 102, 102, 102, 102, 102, 21, 103, 102, 23, 102, 103, 60, 64, 102, 102, 103, 103, 102, 102, 103, 100, 103, 60, 103, 102, 102, 102, 65, 102, 102, 103, 103, 102, 102, 103, 102, 103, 100, 100, 103, 103, 103, 115, 95, 2433598414, 101, 3592554387, 66, 102, 103, 102, 103, 101, 102, 102, 2228097813, 103, 103, 102, 95, 102, 71, 101, 70, 103, 101, 103, 102, 101, 103, 103, 103, 145241642, 103, 21, 100, 68, 95, 103, 95, 103, 102, 1523345737, 102, 102, 103, 101, 26, 102, 115, 96, 33991, 97, 38419, 77, 116, 95, 4062206961, 96, 62964, 97, 26251, 77, 148, 72, 61, 729298391, 66, 103, 103, 103, 102, 62, 103, 103, 144494317, 103, 103, 103, 61, 103, 73, 23, 61, 520292287, 103, 103, 23, 62, 74, 103, 69, 103, 102, 103, 103, 103, 21, 102, 103, 23, 103, 102, 63, 64, 103, 102, 103, 103, 103, 103, 102, 100, 103, 63, 102, 102, 103, 102, 65, 103, 102, 102, 102, 103, 102, 102, 102, 102, 100, 100, 102, 102, 103, 62, 103, 32873, 100, 102, 102, 100, 103, 115, 95, 618453286, 101, 1777402563, 66, 102, 102, 102, 103, 101, 102, 103, 4209324257, 102, 102, 102, 95, 103, 75, 101, 67, 100, 102, 102, 2, 102, 101, 102, 101, 103, 103, 103, 0, 68, 95, 102, 95, 102, 102, 3064829962, 102, 102, 102, 101, 38, 102, 115, 96, 54839, 97, 52850, 77, 116, 95, 382246249, 96, 53045, 97, 29270, 77, 148, 76, 64, 3524578864, 68, 65, 103, 23, 103, 103, 2072268796, 103, 103, 103, 64, 17, 103, 61, 1926402110, 103, 102, 23, 65, 77, 103, 69, 103, 102, 102, 103, 103, 21, 103, 103, 23, 103, 103, 66, 64, 102, 102, 102, 103, 102, 103, 103, 100, 103, 66, 103, 103, 102, 103, 65, 102, 102, 102, 102, 102, 103, 102, 103, 102, 100, 100, 103, 103, 103, 115, 95, 1316429478, 101, 2475374075, 66, 102, 102, 102, 103, 101, 103, 103, 4040571772, 102, 102, 103, 95, 103, 78, 101, 70, 103, 100, 102, 103, 101, 103, 102, 103, 3064829962, 102, 38, 101, 68, 95, 102, 95, 102, 103, 1523345737, 103, 102, 102, 101, 26, 103, 115, 96, 48106, 97, 51164, 77, 116, 95, 2700461522, 96, 53637, 97, 61772, 77, 148, 79, 67, 1730895830, 68, 68, 102, 23, 102, 103, 1273912613, 103, 102, 102, 67, 36, 103, 61, 172174205, 102, 102, 23, 68, 80, 102, 69, 103, 102, 103, 103, 103, 21, 103, 102, 23, 103, 102, 69, 64, 103, 103, 103, 103, 103, 103, 103, 100, 102, 69, 103, 102, 103, 103, 65, 103, 103, 102, 103, 103, 103, 103, 103, 102, 100, 100, 103, 102, 103, 62, 102, 44205, 100, 103, 102, 100, 102, 115, 95, 1710982599, 101, 2869941970, 66, 102, 103, 102, 103, 101, 102, 102, 2863002646, 102, 102, 103, 95, 102, 81, 101, 67, 101, 102, 102, 2, 103, 101, 103, 100, 103, 103, 103, 0, 68, 95, 103, 95, 103, 103, 3013512059, 102, 102, 103, 101, 22, 103, 115, 96, 26616, 97, 62514, 77, 116, 95, 2881507172, 96, 41512, 97, 12528, 77, 148, 82, 70, 3654587460, 68, 71, 103, 23, 102, 102, 3608460499, 102, 102, 103, 70, 83, 102, 61, 3632614277, 102, 102, 23, 71, 84, 103, 69, 103, 102, 102, 102, 102, 21, 102, 102, 23, 102, 102, 72, 64, 102, 102, 102, 102, 103, 103, 103, 100, 103, 72, 103, 102, 103, 103, 65, 103, 103, 102, 103, 103, 102, 102, 103, 102, 100, 100, 102, 103, 103, 115, 95, 1409198547, 101, 2568134870, 66, 103, 102, 103, 102, 101, 102, 102, 2041874477, 103, 103, 102, 95, 103, 85, 101, 70, 103, 101, 103, 102, 101, 102, 102, 102, 145241642, 102, 21, 100, 68, 95, 102, 95, 103, 102, 3013512059, 103, 103, 102, 101, 22, 102, 115, 96, 31573, 97, 15386, 77, 116, 95, 3157332536, 96, 33019, 97, 45198, 77, 148, 86, 73, 896703401, 66, 103, 102, 102, 102, 74, 102, 102, 3267053208, 102, 102, 103, 73, 102, 87, 23, 61, 1848966285, 103, 103, 23, 74, 88, 103, 69, 102, 102, 102, 102, 103, 21, 102, 102, 23, 103, 103, 75, 64, 102, 103, 102, 103, 103, 103, 103, 100, 103, 75, 102, 103, 102, 102, 65, 103, 102, 102, 103, 103, 103, 103, 103, 103, 100, 100, 102, 103, 102, 62, 103, 60628, 100, 102, 102, 100, 102, 115, 95, 2649695276, 101, 3808649014, 66, 103, 102, 102, 103, 101, 102, 103, 282159611, 103, 103, 102, 95, 103, 89, 101, 67, 101, 103, 103, 1, 102, 101, 103, 100, 102, 103, 102, 0, 68, 95, 102, 95, 102, 103, 2157832968, 102, 103, 103, 101, 64, 102, 115, 96, 49492, 97, 46275, 77, 116, 95, 3120218418, 96, 35946, 97, 14868, 77, 148, 90, 76, 2150329853, 68, 77, 103, 23, 102, 102, 3970119660, 103, 102, 103, 76, 91, 103, 61, 584525081, 103, 103, 23, 77, 92, 103, 69, 102, 102, 102, 103, 103, 21, 102, 102, 23, 103, 102, 78, 64, 102, 103, 103, 102, 102, 102, 103, 100, 102, 78, 103, 103, 103, 102, 65, 102, 102, 102, 103, 103, 102, 102, 102, 103, 100, 100, 102, 102, 102, 115, 95, 3669371666, 101, 533356687, 66, 103, 102, 102, 103, 101, 103, 102, 253911283, 103, 102, 102, 95, 103, 93, 101, 70, 103, 101, 103, 103, 101, 103, 103, 103, 2157832968, 102, 64, 100, 68, 95, 103, 95, 103, 103, 145241642, 103, 103, 103, 101, 21, 103, 115, 96, 11173, 97, 14130, 77, 116, 95, 3053615601, 96, 34116, 97, 1506, 77, 148, 94, 79, 1618856535, 68, 80, 103, 23, 103, 103, 3583047915, 102, 102, 102, 79, 46, 103, 61, 85825663, 103, 103, 23, 80, 95, 102, 69, 102, 103, 102, 102, 103, 21, 102, 103, 23, 103, 102, 81, 64, 102, 102, 102, 103, 102, 103, 103, 100, 102, 81, 102, 102, 102, 103, 65, 103, 103, 103, 103, 102, 102, 102, 102, 102, 100, 100, 103, 102, 103, 115, 95, 1875232334, 101, 3034181179, 66, 102, 102, 103, 103, 101, 103, 103, 2695803233, 103, 102, 103, 95, 102, 96, 101, 70, 102, 101, 102, 103, 101, 103, 102, 102, 1523345737, 102, 26, 100, 68, 95, 102, 95, 102, 102, 854373684, 102, 102, 102, 101, 51, 103, 115, 96, 1542, 97, 9767, 77, 116, 95, 2563495154, 96, 34086, 97, 58527, 77, 148, 97, 82, 3498443927, 66, 102, 103, 103, 102, 83, 102, 103, 1812535061, 103, 102, 103, 82, 103, 98, 23, 61, 3899131608, 103, 103, 23, 83, 99, 103, 69, 102, 103, 103, 103, 103, 21, 103, 103, 23, 103, 103, 84, 64, 103, 103, 103, 103, 102, 102, 102, 100, 103, 84, 103, 103, 102, 103, 65, 103, 103, 102, 103, 103, 103, 102, 103, 103, 100, 100, 102, 103, 103, 115, 95, 4111524222, 101, 975513957, 66, 102, 102, 103, 103, 101, 103, 103, 4142074340, 103, 103, 103, 95, 103, 100, 101, 70, 103, 101, 103, 103, 101, 103, 102, 103, 1273912613, 103, 36, 100, 68, 95, 103, 95, 102, 103, 2072268796, 102, 103, 102, 101, 17, 102, 115, 96, 52583, 97, 7998, 77, 116, 95, 214257953, 96, 28546, 97, 7711, 77, 148, 101, 85, 3164350255, 68, 86, 102, 23, 103, 103, 3013512059, 103, 103, 102, 85, 22, 102, 61, 869983517, 102, 102, 23, 86, 102, 102, 69, 103, 103, 102, 102, 102, 21, 103, 103, 23, 103, 103, 87, 64, 102, 102, 102, 103, 103, 102, 102, 100, 102, 87, 102, 102, 102, 103, 65, 102, 102, 102, 102, 102, 102, 103, 103, 102, 100, 100, 103, 103, 103, 62, 103, 56724, 100, 103, 103, 100, 102, 115, 95, 2721339258, 101, 3880285031, 66, 103, 102, 103, 102, 101, 102, 102, 887736975, 103, 103, 103, 95, 103, 103, 101, 67, 101, 102, 103, 1, 102, 101, 103, 100, 102, 103, 102, 2, 68, 95, 102, 95, 102, 102, 3392015959, 103, 102, 103, 101, 104, 102, 115, 96, 18219, 97, 57651, 77, 116, 95, 3712032507, 96, 49605, 97, 52516, 77, 148, 105, 88, 349348863, 68, 89, 102, 23, 103, 102, 2245839489, 103, 102, 102, 88, 31, 102, 61, 2962585207, 103, 102, 23, 89, 106, 102, 69, 103, 102, 102, 102, 102, 21, 103, 102, 23, 103, 103, 90, 64, 102, 103, 103, 103, 103, 103, 103, 100, 103, 90, 102, 102, 102, 103, 65, 103, 103, 103, 102, 103, 102, 102, 103, 102, 100, 100, 103, 103, 103, 115, 95, 3224842266, 101, 88824460, 66, 102, 103, 102, 102, 101, 102, 102, 3413205279, 103, 102, 103, 95, 103, 107, 101, 70, 102, 100, 103, 103, 101, 103, 103, 102, 1523345737, 103, 26, 101, 68, 95, 102, 95, 103, 103, 2245839489, 102, 103, 102, 101, 31, 103, 115, 96, 1252, 97, 52998, 77, 116, 95, 2354142184, 96, 58005, 97, 21761, 77, 148, 108, 91, 2642890533, 66, 102, 102, 103, 103, 92, 103, 103, 1248103663, 103, 103, 103, 91, 103, 109, 23, 61, 64170660, 102, 102, 23, 92, 110, 102, 69, 103, 102, 103, 102, 103, 21, 102, 103, 23, 102, 102, 93, 64, 102, 103, 102, 103, 102, 102, 103, 100, 103, 93, 102, 103, 103, 102, 65, 103, 102, 103, 102, 103, 103, 103, 103, 102, 100, 100, 102, 103, 102, 115, 95, 3432855118, 101, 296835147, 66, 102, 103, 102, 103, 101, 103, 103, 3796292179, 102, 102, 102, 95, 103, 111, 101, 70, 103, 101, 102, 102, 101, 102, 103, 103, 3970119660, 103, 91, 100, 68, 95, 103, 95, 102, 102, 3583047915, 102, 103, 102, 101, 46, 102, 115, 96, 29154, 97, 50448, 77, 116, 95, 554886800, 96, 10491, 97, 33602, 77, 116, 95, 2255275798, 96, 39497, 97, 12005, 77, 148, 5, 106, 3544597836, 68, 9, 102, 0, 102, 102, 145241642, 102, 102, 103, 106, 21, 103, 55, 149, 39999, 94, 68, 21, 103, 21, 103, 103, 2072268796, 103, 103, 103, 94, 17, 103, 116, 95, 2206478295, 96, 62530, 97, 11260, 77, 149, 45981, 94, 71, 102, 102, 102, 94, 102, 102, 102, 94, 21, 102, 103, 102, 64, 102, 102, 103, 102, 102, 102, 102, 100, 102, 94, 102, 102, 103, 102, 65, 102, 103, 103, 102, 103, 102, 103, 102, 102, 100, 100, 103, 102, 102, 115, 95, 1485548177, 101, 2303661833, 66, 102, 102, 102, 103, 101, 103, 102, 2751129129, 103, 102, 102, 95, 102, 112, 101, 70, 102, 101, 103, 103, 101, 102, 102, 102, 2157832968, 102, 64, 100, 68, 95, 103, 95, 102, 102, 3970119660, 102, 102, 103, 101, 91, 102, 115, 96, 2768, 97, 18258, 77, 149, 8554, 94, 68, 21, 103, 21, 102, 102, 3392015959, 102, 103, 103, 94, 104, 103, 116, 95, 2942539273, 96, 23484, 97, 22492, 77, 149, 19480, 94, 66, 103, 102, 102, 103, 21, 102, 102, 1872711851, 102, 102, 103, 94, 102, 113, 21, 116, 95, 932239856, 96, 47446, 97, 57353, 77, 149, 47085, 21, 148, 7, 106, 0, 96, 55, 115, 4, 3515, 94, 50935, 66, 103, 103, 103, 103, 21, 103, 103, 1648967809, 103, 103, 103, 94, 103, 114, 21, 116, 95, 1102275283, 96, 5314, 97, 59948, 77, 61, 3106109214, 102, 102, 5, 9, 115, 102, 149, 4339, 4, 55, 149, 32862, 94, 66, 102, 102, 103, 102, 21, 103, 102, 3867584096, 102, 102, 102, 94, 102, 116, 21, 116, 95, 1381252359, 96, 11102, 97, 64205, 77, 71, 103, 103, 102, 106, 103, 102, 102, 6, 0, 103, 103, 102, 64, 102, 103, 102, 102, 102, 102, 102, 100, 103, 106, 102, 102, 102, 102, 65, 103, 103, 102, 102, 103, 102, 103, 102, 103, 100, 100, 103, 103, 102, 115, 95, 148274745, 101, 4127661110, 66, 103, 102, 102, 102, 101, 102, 102, 606680739, 102, 103, 103, 95, 102, 117, 101, 70, 102, 100, 102, 102, 101, 102, 103, 102, 3583047915, 103, 46, 101, 68, 95, 102, 95, 103, 103, 2245839489, 103, 103, 103, 101, 31, 102, 115, 96, 25365, 97, 15941, 77, 149, 55883, 94, 66, 103, 102, 103, 103, 21, 103, 103, 3635252865, 102, 102, 103, 94, 102, 118, 21, 116, 95, 2719621897, 96, 64785, 97, 19090, 77, 149, 12993, 94, 66, 103, 103, 102, 102, 21, 102, 102, 1187698124, 102, 102, 103, 94, 102, 119, 21, 116, 95, 1709232745, 96, 48763, 97, 3673, 77, 97, 98, 120, 6, 10, 1121916860, 94, 28034, 68, 21, 103, 21, 102, 102, 2245839489, 103, 102, 103, 94, 31, 102, 116, 95, 2643245906, 96, 39293, 97, 17925, 77, 149, 38155, 21, 80, 149, 62544, 94, 71, 102, 103, 103, 94, 102, 102, 103, 94, 21, 103, 103, 103, 64, 102, 103, 102, 102, 102, 102, 102, 100, 103, 94, 102, 103, 103, 103, 65, 103, 103, 102, 103, 102, 102, 103, 102, 103, 100, 100, 103, 102, 102, 62, 103, 41826, 100, 103, 103, 100, 102, 115, 95, 3388371402, 101, 56380007, 66, 102, 103, 103, 102, 101, 103, 102, 1180526578, 103, 103, 103, 95, 102, 120, 101, 67, 101, 102, 102, 0, 102, 101, 103, 100, 102, 102, 103, 0, 68, 95, 102, 95, 102, 102, 3392015959, 103, 103, 102, 101, 104, 102, 115, 96, 30112, 97, 10246, 77, 81, 66, 102, 102, 102, 103, 21, 103, 103, 1805974780, 102, 102, 102, 94, 102, 121, 21, 116, 95, 1372496884, 96, 21483, 97, 64071, 77, 149, 36891, 94, 68, 21, 103, 21, 102, 103, 1523345737, 102, 102, 102, 94, 26, 103, 116, 95, 374059448, 96, 18863, 97, 48836, 77, 149, 3771, 4, 55, 149, 3206, 94, 68, 21, 103, 21, 103, 102, 2072268796, 102, 103, 103, 94, 17, 102, 116, 95, 3272040328, 96, 13268, 97, 27520, 77, 139, 21, 8710, 16, 1637342921, 17, 141, 18, 7, 149, 741, 94, 68, 21, 103, 21, 103, 102, 2211313669, 103, 102, 103, 94, 41, 102, 116, 95, 3051530431, 96, 26565, 97, 24155, 77, 149, 61186, 21, 99, 42, 10, 15, 4, 14, 16, 17, 18, 100, 61, 3946574148, 102, 102, 5, 106, 122, 102, 149, 1142, 94, 66, 103, 102, 102, 102, 21, 103, 103, 2418237762, 102, 102, 103, 94, 102, 123, 21, 116, 95, 1192084076, 96, 46643, 97, 61318, 77, 149, 41146, 94, 71, 102, 103, 103, 94, 102, 102, 102, 94, 21, 102, 103, 102, 64, 103, 102, 103, 102, 102, 102, 103, 100, 103, 94, 103, 102, 103, 103, 65, 103, 102, 102, 103, 102, 103, 103, 103, 102, 100, 100, 102, 103, 103, 62, 102, 35552, 100, 103, 103, 100, 102, 115, 95, 1741666352, 101, 2559773352, 66, 103, 103, 103, 102, 101, 102, 102, 2234974204, 103, 103, 102, 95, 102, 124, 101, 67, 100, 102, 102, 0, 102, 101, 102, 101, 102, 102, 103, 2, 68, 95, 103, 95, 103, 103, 3316678665, 102, 102, 103, 101, 43, 102, 115, 96, 27555, 97, 39227, 77, 149, 719, 94, 68, 21, 103, 21, 103, 102, 1273912613, 103, 102, 103, 94, 36, 102, 116, 95, 4053087796, 96, 52765, 97, 39437, 77, 149, 2585, 94, 68, 21, 103, 21, 102, 103, 1855925594, 102, 102, 103, 94, 125, 102, 116, 95, 3655130295, 96, 43212, 97, 33365, 77, 81, 68, 21, 103, 21, 103, 103, 2157832968, 103, 103, 103, 94, 64, 103, 116, 95, 2494467013, 96, 19218, 97, 15655, 77, 149, 11515, 21, 101, 116, 16, 501063321, 17, 124, 94, 34118, 68, 21, 102, 21, 103, 103, 1857505674, 103, 103, 103, 94, 55, 102, 116, 95, 1632473656, 96, 32111, 97, 2502, 77, 82, 42, 12, 15, 4, 14, 16, 17, 18, 120, 13, 126, 0, 94, 14951, 68, 21, 103, 21, 102, 102, 3013512059, 103, 103, 103, 94, 22, 103, 116, 95, 3445177285, 96, 3325, 97, 30162, 77, 102, 116, 16, 3194894967, 17, 132, 94, 16804, 68, 21, 102, 21, 103, 102, 1855925594, 103, 103, 103, 94, 125, 102, 116, 95, 1266753162, 96, 52016, 97, 62457, 77, 55, 149, 31630, 94, 68, 21, 103, 21, 102, 102, 2157832968, 102, 102, 103, 94, 64, 102, 116, 95, 2264123417, 96, 39784, 97, 12140, 77, 149, 6492, 21, 80, 82, 42, 8, 15, 4, 14, 16, 17, 18, 44, 106, 7, 1, 8, 103, 104, 47, 15, 7517, 4, 87, 0, 0, 105, 149, 13991, 94, 68, 21, 102, 21, 102, 103, 1855925594, 102, 102, 102, 94, 125, 102, 116, 95, 3080274316, 96, 4404, 97, 24594, 77, 106, 107, 149, 602, 94, 68, 21, 103, 21, 102, 103, 3064829962, 102, 102, 102, 94, 38, 102, 116, 95, 4256411780, 96, 22165, 97, 42540, 77, 42, 95, 98, 3, 95, 96, 97, 148, 127, 99, 807505853, 108, 109, 119, 79, 128, 1972447401, 77, 129, 895868204, 80, 10, 1121916860, 78, 130, 1328861512, 47, 73, 10943, 3, 16, 0, 0, 110, 148, 131, 16, 2389349697, 61, 1898896230, 77, 77, 17, 16, 132, 77, 148, 10, 18, 1121916860, 61, 1250033165, 78, 78, 19, 18, 133, 78, 148, 134, 20, 3751030266, 63, 78, 20, 78, 17, 4488, 21, 78, 79, 64, 77, 78, 77, 78, 77, 77, 77, 75, 77, 21, 77, 77, 78, 77, 65, 77, 78, 78, 77, 78, 77, 77, 78, 78, 75, 75, 77, 78, 78, 62, 77, 53061, 75, 77, 77, 75, 78, 115, 70, 2289585897, 76, 9282831, 66, 77, 78, 78, 78, 76, 78, 77, 773158336, 77, 78, 77, 70, 78, 135, 76, 67, 76, 77, 77, 1, 78, 76, 77, 75, 77, 78, 78, 0, 68, 70, 78, 70, 78, 77, 854373684, 78, 77, 78, 76, 51, 77, 115, 71, 37692, 72, 21178, 78, 148, 136, 22, 3408445401, 61, 572487661, 77, 77, 19, 22, 137, 78, 69, 77, 77, 77, 78, 77, 17, 78, 77, 19, 78, 78, 23, 64, 77, 78, 77, 77, 78, 77, 78, 75, 78, 23, 78, 78, 78, 78, 65, 77, 78, 77, 78, 78, 78, 78, 77, 78, 75, 75, 78, 77, 77, 115, 70, 3811623583, 76, 1010076655, 66, 78, 77, 77, 77, 76, 77, 78, 1223962435, 78, 78, 77, 70, 78, 138, 76, 70, 78, 76, 77, 78, 76, 78, 78, 78, 3316678665, 77, 43, 75, 68, 70, 78, 70, 77, 78, 1857505674, 78, 77, 77, 76, 55, 78, 115, 71, 57276, 72, 13674, 78, 116, 70, 110240543, 71, 21009, 72, 32073, 78, 148, 139, 24, 4179415464, 68, 25, 77, 19, 77, 77, 1523345737, 77, 77, 77, 24, 26, 77, 61, 3761477563, 77, 77, 19, 25, 140, 78, 69, 78, 77, 78, 77, 77, 17, 78, 78, 19, 78, 77, 26, 64, 77, 77, 77, 78, 78, 77, 78, 75, 77, 26, 77, 78, 78, 77, 65, 77, 78, 77, 78, 77, 78, 77, 78, 77, 75, 75, 78, 78, 78, 115, 70, 2580905603, 76, 594348558, 66, 78, 78, 78, 78, 76, 77, 78, 1125354018, 78, 77, 77, 70, 78, 141, 76, 70, 78, 76, 78, 77, 76, 78, 77, 78, 2671826207, 78, 61, 75, 68, 70, 78, 70, 78, 77, 1857505674, 77, 77, 77, 76, 55, 77, 115, 71, 33706, 72, 6682, 78, 116, 70, 2944226236, 71, 33298, 72, 13332, 78, 148, 142, 27, 3790139381, 66, 77, 77, 77, 78, 28, 78, 78, 2148553643, 78, 77, 78, 27, 78, 143, 19, 61, 1421727777, 78, 77, 19, 28, 144, 77, 69, 77, 77, 77, 78, 77, 17, 78, 77, 19, 77, 77, 29, 64, 78, 77, 77, 78, 77, 77, 77, 75, 77, 29, 77, 77, 78, 78, 65, 77, 77, 78, 77, 77, 77, 77, 77, 77, 75, 75, 77, 77, 77, 62, 78, 21938, 75, 77, 77, 75, 78, 115, 70, 1485590312, 76, 3516075240, 66, 78, 78, 77, 78, 76, 78, 77, 2014652691, 78, 77, 78, 70, 77, 145, 76, 67, 75, 78, 78, 0, 78, 76, 78, 76, 78, 78, 77, 1, 68, 70, 78, 70, 78, 77, 1857505674, 77, 77, 78, 76, 55, 78, 115, 71, 25223, 72, 24866, 78, 116, 70, 711622428, 71, 38100, 72, 56505, 78, 148, 146, 30, 1707511439, 68, 31, 78, 19, 78, 78, 2211313669, 77, 78, 78, 30, 41, 78, 61, 1795021563, 78, 78, 19, 31, 147, 77, 69, 77, 77, 77, 77, 78, 17, 77, 78, 19, 78, 77, 32, 64, 78, 78, 77, 78, 78, 78, 78, 75, 78, 32, 78, 78, 78, 78, 65, 78, 78, 77, 78, 77, 78, 78, 78, 77, 75, 75, 78, 77, 78, 115, 70, 671802759, 76, 1862868039, 66, 77, 78, 78, 77, 76, 78, 78, 4017926662, 77, 77, 78, 70, 78, 148, 76, 70, 77, 76, 77, 77, 76, 77, 77, 77, 1855925594, 77, 125, 75, 68, 70, 77, 70, 77, 77, 3970119660, 78, 78, 78, 76, 91, 77, 115, 71, 6343, 72, 51949, 78, 116, 70, 2852049898, 71, 2712, 72, 27130, 78, 148, 149, 33, 3112552996, 66, 77, 77, 77, 78, 34, 78, 78, 2906034787, 77, 78, 77, 33, 77, 150, 19, 61, 3391905966, 77, 77, 19, 34, 151, 77, 69, 78, 78, 78, 77, 77, 17, 77, 78, 19, 78, 78, 35, 64, 77, 78, 78, 78, 78, 78, 77, 75, 78, 35, 77, 78, 78, 77, 65, 77, 78, 77, 78, 78, 78, 78, 77, 78, 75, 75, 77, 78, 78, 62, 78, 34673, 75, 77, 78, 75, 78, 115, 70, 147912993, 76, 3520281185, 66, 78, 78, 78, 78, 76, 77, 78, 3014731667, 77, 77, 78, 70, 78, 152, 76, 67, 75, 78, 78, 2, 78, 76, 78, 76, 77, 78, 78, 1, 68, 70, 78, 70, 78, 77, 2671826207, 78, 78, 78, 76, 61, 78, 115, 71, 9243, 72, 11707, 78, 116, 70, 2673876022, 71, 59011, 72, 22063, 78, 148, 153, 36, 2050156522, 66, 77, 77, 77, 78, 37, 78, 78, 975184944, 77, 78, 77, 36, 77, 154, 19, 61, 2581440279, 78, 78, 19, 37, 155, 77, 69, 78, 78, 78, 77, 77, 17, 77, 78, 19, 77, 77, 38, 64, 78, 77, 77, 78, 77, 77, 77, 75, 78, 38, 77, 78, 78, 77, 65, 77, 78, 78, 78, 77, 77, 78, 77, 77, 75, 75, 77, 77, 77, 115, 70, 2273885136, 76, 1048766608, 66, 78, 77, 77, 77, 76, 78, 78, 2344766082, 77, 78, 77, 70, 77, 156, 76, 70, 77, 76, 78, 77, 76, 77, 78, 77, 3583047915, 78, 46, 75, 68, 70, 78, 70, 78, 78, 2245839489, 78, 78, 78, 76, 31, 78, 115, 71, 3440, 72, 33342, 78, 116, 70, 3550574727, 71, 46742, 72, 32256, 78, 148, 157, 39, 267500046, 66, 77, 77, 77, 78, 40, 78, 78, 1645581001, 77, 78, 77, 39, 78, 158, 19, 61, 3228252623, 77, 77, 19, 40, 159, 78, 69, 78, 77, 77, 77, 78, 17, 77, 77, 19, 77, 78, 41, 64, 77, 77, 77, 77, 77, 77, 78, 75, 77, 41, 78, 77, 78, 78, 65, 78, 78, 77, 78, 78, 77, 77, 78, 78, 75, 75, 78, 78, 78, 115, 70, 2410271662, 76, 4245780243, 66, 78, 78, 78, 77, 76, 77, 78, 2591152868, 77, 78, 77, 70, 78, 160, 76, 70, 77, 75, 77, 77, 76, 77, 77, 78, 2211313669, 78, 41, 76, 68, 70, 78, 70, 77, 78, 1523345737, 78, 77, 77, 76, 26, 77, 115, 71, 59391, 72, 41063, 78, 116, 70, 3876726821, 71, 55320, 72, 22960, 78, 148, 161, 42, 2666702914, 66, 77, 78, 77, 77, 43, 77, 77, 1700977022, 78, 78, 77, 42, 78, 162, 19, 61, 1653993485, 77, 77, 19, 43, 163, 78, 69, 78, 78, 77, 77, 78, 17, 77, 77, 19, 77, 77, 44, 64, 78, 78, 78, 77, 77, 77, 78, 75, 78, 44, 77, 77, 78, 78, 65, 77, 78, 78, 78, 77, 78, 77, 77, 78, 75, 75, 77, 77, 78, 115, 70, 2876560988, 76, 429930786, 66, 78, 78, 78, 77, 76, 77, 77, 2712568468, 77, 78, 78, 70, 78, 164, 76, 70, 77, 75, 77, 78, 76, 78, 77, 78, 2245839489, 78, 31, 76, 68, 70, 77, 70, 77, 78, 3608460499, 77, 78, 78, 76, 83, 77, 115, 71, 47327, 72, 41057, 78, 116, 70, 1802678921, 71, 42506, 72, 11494, 78, 148, 165, 45, 3856423955, 68, 46, 78, 19, 78, 78, 3013512059, 78, 78, 78, 45, 22, 78, 61, 2700542465, 77, 78, 19, 46, 166, 78, 69, 77, 78, 78, 78, 78, 17, 77, 78, 19, 78, 78, 47, 64, 77, 77, 78, 78, 77, 77, 77, 75, 78, 47, 77, 77, 78, 78, 65, 78, 78, 77, 77, 78, 77, 77, 77, 77, 75, 75, 77, 78, 77, 115, 70, 1359024019, 76, 2665042741, 66, 78, 78, 78, 78, 76, 77, 77, 4217994541, 77, 77, 78, 70, 77, 167, 76, 70, 78, 75, 78, 78, 76, 78, 77, 77, 3316678665, 77, 43, 76, 68, 70, 78, 70, 77, 77, 1855925594, 77, 78, 78, 76, 125, 78, 115, 71, 9786, 72, 39231, 78, 116, 70, 2399096420, 71, 9242, 72, 34579, 78, 148, 168, 48, 4288857995, 66, 77, 77, 78, 77, 49, 77, 77, 2875668961, 78, 77, 78, 48, 77, 169, 19, 61, 3766821798, 78, 78, 19, 49, 170, 78, 69, 78, 77, 78, 77, 78, 17, 78, 77, 19, 77, 78, 50, 64, 78, 78, 77, 78, 78, 77, 78, 75, 77, 50, 78, 77, 77, 77, 65, 77, 77, 77, 77, 78, 77, 77, 78, 78, 75, 75, 77, 77, 77, 62, 78, 32716, 75, 77, 77, 75, 77, 115, 70, 274699666, 76, 2142776837, 66, 77, 77, 77, 77, 76, 77, 78, 1404835917, 77, 77, 77, 70, 78, 171, 76, 67, 76, 77, 77, 2, 78, 76, 78, 75, 78, 77, 78, 0, 68, 70, 77, 70, 77, 78, 3970119660, 77, 78, 78, 76, 91, 77, 115, 71, 38745, 72, 17774, 78, 116, 70, 2350233302, 71, 13571, 72, 59448, 78, 148, 172, 51, 939409377, 66, 77, 77, 78, 77, 52, 77, 77, 4217777561, 78, 77, 77, 51, 77, 173, 19, 61, 1871845464, 77, 78, 19, 52, 174, 77, 69, 78, 77, 78, 77, 78, 17, 77, 77, 19, 78, 78, 53, 64, 78, 78, 77, 78, 77, 78, 77, 75, 77, 53, 77, 78, 77, 78, 65, 78, 78, 78, 77, 77, 77, 77, 77, 77, 75, 75, 77, 77, 77, 115, 70, 3597819858, 76, 1758065055, 66, 78, 78, 78, 77, 76, 77, 77, 3575825927, 78, 78, 78, 70, 78, 175, 76, 70, 78, 75, 78, 78, 76, 78, 77, 78, 2211313669, 77, 41, 76, 68, 70, 77, 70, 77, 78, 1855925594, 77, 77, 77, 76, 125, 78, 115, 71, 34260, 72, 54186, 78, 116, 70, 1045442137, 71, 1500, 72, 4340, 78, 148, 176, 54, 2301201251, 68, 55, 78, 19, 77, 78, 3583047915, 78, 78, 77, 54, 46, 78, 61, 1925195217, 78, 77, 19, 55, 177, 77, 69, 77, 77, 77, 78, 77, 17, 77, 77, 19, 77, 77, 56, 64, 77, 77, 77, 77, 78, 78, 77, 75, 77, 56, 77, 77, 77, 77, 65, 77, 77, 77, 77, 77, 77, 77, 78, 78, 75, 75, 77, 77, 77, 62, 77, 53145, 75, 77, 78, 75, 77, 115, 70, 2920776087, 76, 3609544662, 66, 77, 77, 78, 78, 76, 77, 78, 3004178279, 77, 77, 77, 70, 77, 178, 76, 67, 75, 78, 77, 2, 78, 76, 78, 76, 78, 77, 77, 1, 68, 70, 77, 70, 77, 78, 3064829962, 78, 77, 78, 76, 38, 78, 115, 71, 27613, 72, 20356, 78, 116, 70, 1482099091, 71, 39858, 72, 58635, 78, 148, 179, 57, 1102061233, 66, 77, 78, 77, 78, 58, 78, 78, 3203365588, 78, 77, 77, 57, 77, 180, 19, 61, 258921313, 77, 78, 19, 58, 181, 77, 69, 78, 78, 78, 78, 78, 17, 77, 77, 19, 77, 77, 59, 64, 77, 77, 77, 77, 78, 77, 78, 75, 77, 59, 78, 77, 78, 78, 65, 78, 77, 78, 78, 78, 77, 78, 78, 78, 75, 75, 78, 78, 78, 115, 70, 2306360601, 76, 4247770083, 66, 78, 78, 77, 78, 76, 78, 78, 778930553, 77, 77, 78, 70, 78, 182, 76, 70, 77, 75, 78, 77, 76, 78, 78, 78, 854373684, 77, 51, 76, 68, 70, 78, 70, 77, 78, 3608460499, 77, 77, 77, 76, 83, 78, 115, 71, 41492, 72, 36172, 78, 116, 70, 3130644197, 71, 24958, 72, 45074, 78, 148, 183, 60, 4230544491, 68, 61, 77, 19, 78, 78, 1857505674, 78, 77, 77, 60, 55, 78, 61, 148856212, 77, 77, 19, 61, 184, 78, 69, 77, 78, 78, 78, 77, 17, 77, 77, 19, 77, 78, 62, 64, 77, 77, 77, 77, 78, 77, 77, 75, 78, 62, 77, 77, 78, 77, 65, 77, 77, 77, 77, 77, 78, 78, 77, 77, 75, 75, 77, 77, 77, 62, 78, 11200, 75, 77, 77, 75, 78, 115, 70, 1882491725, 76, 2568868236, 66, 77, 77, 77, 77, 76, 78, 78, 3454777371, 77, 78, 78, 70, 77, 185, 76, 67, 76, 78, 77, 1, 77, 76, 78, 75, 77, 78, 78, 2, 68, 70, 77, 70, 77, 78, 3013512059, 77, 77, 78, 76, 22, 77, 115, 71, 39847, 72, 19382, 78, 116, 70, 2584629021, 71, 62484, 72, 12825, 78, 148, 186, 63, 2163094214, 66, 77, 77, 77, 77, 64, 77, 77, 4151147810, 77, 77, 77, 63, 77, 187, 19, 61, 3250609091, 77, 77, 19, 64, 188, 77, 69, 78, 78, 78, 77, 77, 17, 77, 77, 19, 77, 78, 65, 64, 78, 77, 77, 77, 77, 78, 77, 75, 78, 65, 77, 77, 77, 77, 65, 78, 77, 78, 77, 77, 78, 77, 77, 78, 75, 75, 77, 78, 77, 115, 70, 3150846939, 76, 1311735116, 66, 78, 78, 78, 78, 76, 78, 77, 275995303, 78, 78, 77, 70, 78, 189, 76, 70, 78, 76, 78, 77, 76, 77, 78, 77, 2157832968, 78, 64, 75, 68, 70, 77, 70, 78, 77, 3583047915, 78, 77, 77, 76, 46, 78, 115, 71, 27634, 72, 37476, 78, 116, 70, 942630861, 71, 52744, 72, 55485, 78, 148, 190, 66, 2902054478, 66, 78, 78, 78, 78, 67, 78, 78, 290787678, 77, 78, 78, 66, 78, 191, 19, 61, 1449699441, 78, 77, 19, 67, 192, 77, 69, 78, 78, 78, 77, 78, 17, 78, 78, 19, 78, 77, 68, 64, 77, 78, 78, 78, 78, 77, 78, 75, 78, 68, 78, 78, 77, 77, 65, 77, 77, 78, 78, 77, 78, 77, 77, 77, 75, 75, 78, 77, 78, 115, 70, 1598213577, 76, 3809226483, 66, 78, 78, 77, 77, 76, 78, 77, 3740743898, 78, 78, 77, 70, 77, 193, 76, 70, 77, 76, 77, 77, 76, 77, 78, 77, 2072268796, 78, 17, 75, 68, 70, 77, 70, 77, 77, 2245839489, 77, 77, 78, 76, 31, 77, 115, 71, 62100, 72, 21810, 78, 116, 70, 1423250668, 71, 54819, 72, 23900, 78, 116, 70, 2269562060, 71, 56704, 72, 42187, 78, 148, 194, 81, 1242271441, 68, 82, 77, 5, 77, 77, 1855925594, 77, 77, 77, 81, 125, 77, 61, 1742533507, 77, 77, 5, 82, 195, 78, 149, 13231, 69, 68, 17, 77, 17, 77, 77, 854373684, 77, 77, 78, 69, 51, 77, 116, 70, 3798721358, 71, 47847, 72, 49646, 78, 149, 35797, 69, 71, 77, 77, 77, 69, 78, 78, 78, 69, 17, 77, 78, 78, 64, 77, 77, 77, 78, 78, 78, 78, 75, 77, 69, 78, 78, 77, 77, 65, 78, 78, 78, 78, 77, 78, 78, 77, 78, 75, 75, 78, 78, 77, 115, 70, 1354008284, 76, 1459775004, 66, 78, 78, 77, 78, 76, 78, 77, 1324688066, 77, 77, 78, 70, 77, 196, 76, 70, 77, 75, 77, 77, 76, 77, 77, 78, 2245839489, 78, 31, 76, 68, 70, 77, 70, 78, 77, 2211313669, 78, 78, 78, 76, 41, 78, 115, 71, 32907, 72, 14636, 78, 149, 8674, 69, 66, 77, 78, 77, 77, 17, 77, 77, 858274059, 77, 77, 77, 69, 77, 197, 17, 116, 70, 3861885675, 71, 59748, 72, 49698, 78, 149, 59144, 69, 66, 77, 77, 78, 77, 17, 77, 77, 2987063414, 78, 77, 77, 69, 78, 198, 17, 116, 70, 1874701138, 71, 59081, 72, 19548, 78, 148, 199, 81, 4281217317, 111, 43, 10, 3, 82, 1, 5, 149, 29158, 69, 66, 78, 78, 78, 77, 17, 77, 77, 514088084, 77, 78, 77, 69, 77, 200, 17, 116, 70, 3122519403, 71, 46661, 72, 63036, 78, 68, 81, 78, 5, 78, 77, 3316678665, 77, 78, 78, 13, 43, 78, 43, 12, 3, 11, 1, 81, 120, 14, 201, 3115410507, 69, 5952, 68, 17, 77, 17, 77, 77, 3064829962, 77, 77, 78, 69, 38, 77, 116, 70, 3754306376, 71, 24896, 72, 56404, 78, 149, 19570, 69, 66, 77, 77, 77, 77, 17, 77, 77, 4246093424, 77, 77, 78, 69, 77, 202, 17, 116, 70, 3981066339, 71, 18355, 72, 51912, 78, 148, 10, 81, 1121916860, 61, 2283691805, 77, 77, 5, 81, 203, 78, 149, 24080, 69, 68, 17, 77, 17, 77, 78, 2072268796, 77, 78, 77, 69, 17, 78, 116, 70, 1683902414, 71, 16790, 72, 17404, 78, 140, 81, 199, 4281217317, 11, 3, 81, 13, 5, 3544597836, 69, 16890, 68, 17, 77, 17, 78, 77, 2245839489, 77, 77, 78, 69, 31, 78, 116, 70, 1242947117, 71, 45575, 72, 26116, 78, 148, 7, 81, 0, 112, 68, 81, 77, 4, 77, 78, 2157832968, 78, 78, 77, 9, 64, 77, 61, 3041207266, 78, 78, 4, 81, 204, 77, 149, 31338, 69, 66, 77, 77, 77, 78, 17, 78, 78, 204703047, 77, 77, 78, 69, 78, 205, 17, 116, 70, 26094695, 71, 25109, 72, 16012, 78, 61, 2482448448, 77, 77, 3, 6, 206, 78, 148, 207, 81, 1273901866, 61, 3337502165, 77, 78, 4, 81, 208, 78, 149, 2548, 69, 66, 77, 78, 77, 78, 17, 78, 78, 4265055541, 78, 77, 77, 69, 78, 209, 17, 116, 70, 636116045, 71, 12572, 72, 616, 78, 149, 50470, 69, 68, 17, 78, 17, 77, 78, 3970119660, 78, 77, 77, 69, 91, 77, 116, 70, 3061926008, 71, 16758, 72, 62865, 78, 150, 4, 124, 7, 51, 854373684, 81, 210, 4086084229, 8, 7, 81, 149, 28135, 69, 68, 17, 77, 17, 77, 78, 3608460499, 77, 78, 78, 69, 83, 77, 116, 70, 902556003, 71, 42615, 72, 29262, 78, 72, 12, 78, 77, 78, 78, 78, 78, 81, 14, 77, 78, 73, 82, 77, 78, 3354588857, 78, 78, 78, 81, 78, 78, 211, 10, 77, 77, 43, 9, 7, 8, 1, 82, 149, 3618, 69, 66, 77, 77, 78, 78, 17, 77, 77, 233025887, 77, 78, 78, 69, 77, 212, 17, 116, 70, 3514285192, 71, 54461, 72, 61082, 78, 121, 81, 213, 1344217311, 82, 3, 81, 71, 77, 77, 77, 81, 77, 77, 77, 82, 5, 78, 77, 78, 64, 77, 78, 78, 78, 78, 77, 78, 75, 78, 81, 77, 77, 78, 78, 65, 78, 77, 78, 77, 77, 77, 78, 78, 77, 75, 75, 77, 78, 78, 115, 70, 2344406204, 76, 4138065020, 66, 78, 77, 78, 78, 76, 77, 77, 3184174747, 77, 77, 78, 70, 77, 214, 76, 70, 78, 76, 78, 77, 76, 78, 77, 78, 3970119660, 78, 91, 75, 68, 70, 78, 70, 77, 78, 3013512059, 77, 77, 78, 76, 22, 77, 115, 71, 49949, 72, 8628, 78, 149, 20398, 69, 68, 17, 78, 17, 77, 78, 3064829962, 77, 78, 78, 69, 38, 77, 116, 70, 2818806162, 71, 2668, 72, 33815, 78, 149, 94, 69, 66, 77, 77, 78, 78, 17, 78, 78, 2070739557, 77, 77, 77, 69, 78, 215, 17, 116, 70, 3904785500, 71, 17315, 72, 51036, 78, 151, 2, 152, 216, 2023235878, 81, 42, 6, 81, 1, 0, 149, 5458, 69, 66, 77, 77, 77, 78, 17, 77, 78, 492044408, 77, 77, 77, 69, 78, 217, 17, 116, 70, 1324379280, 71, 12177, 72, 28002, 78, 42, 70, 73, 3, 70, 71, 72, 121, 74, 218, 2817017979, 70, 70, 74, 113, 128, 81, 219, 4143210642, 80, 220, 671634047, 83, 221, 3924059938, 84, 10, 1121916860, 82, 222, 3683185917, 47, 77, 11303, 2, 14, 0, 0, 126, 20, 21, 223, 1706366714, 61, 4149959106, 82, 82, 22, 21, 224, 82, 148, 10, 23, 1121916860, 61, 1510637992, 82, 81, 24, 23, 225, 82, 148, 226, 25, 3542545297, 63, 81, 25, 81, 22, 7578, 26, 81, 83, 64, 80, 80, 81, 81, 81, 82, 82, 78, 80, 26, 80, 80, 80, 82, 65, 80, 82, 82, 80, 80, 81, 82, 81, 82, 78, 78, 81, 82, 82, 115, 75, 14863492, 79, 4067777114, 66, 82, 82, 82, 80, 79, 80, 80, 3945147159, 80, 82, 81, 75, 82, 227, 79, 70, 82, 79, 80, 80, 79, 80, 81, 81, 3392015959, 82, 104, 78, 68, 75, 80, 75, 82, 82, 2245839489, 80, 80, 80, 79, 31, 81, 149, 678668016, 76, 79, 148, 228, 27, 1262946183, 61, 3672823991, 81, 81, 24, 27, 229, 81, 69, 81, 81, 82, 82, 82, 22, 81, 81, 24, 81, 82, 28, 64, 82, 82, 82, 82, 81, 81, 82, 78, 82, 28, 82, 81, 82, 82, 65, 82, 81, 80, 80, 81, 81, 81, 82, 81, 78, 78, 81, 81, 82, 62, 82, 23161, 78, 82, 80, 78, 81, 115, 75, 1000905417, 79, 2837881755, 66, 82, 82, 82, 82, 79, 82, 80, 2072124736, 81, 82, 81, 75, 81, 230, 79, 67, 79, 82, 82, 1, 81, 79, 81, 78, 82, 82, 81, 2, 68, 75, 81, 75, 81, 80, 3316678665, 82, 80, 82, 79, 43, 81, 149, 3582505189, 76, 79, 115, 75, 2904313189, 76, 517980770, 79, 148, 231, 29, 122318599, 66, 80, 81, 82, 81, 30, 81, 81, 1445783981, 82, 82, 80, 29, 82, 232, 24, 61, 2324400805, 81, 80, 24, 30, 233, 82, 69, 81, 81, 82, 81, 82, 22, 82, 80, 24, 82, 81, 31, 64, 82, 81, 82, 81, 80, 80, 80, 78, 81, 31, 82, 82, 81, 82, 65, 80, 80, 82, 82, 80, 82, 82, 82, 80, 78, 78, 80, 80, 82, 115, 75, 1736138496, 79, 2445624926, 66, 82, 81, 81, 80, 79, 82, 82, 3410753738, 82, 82, 82, 75, 80, 234, 79, 70, 80, 79, 82, 80, 79, 82, 82, 80, 2671826207, 80, 61, 78, 68, 75, 82, 75, 82, 80, 2245839489, 82, 80, 82, 79, 31, 80, 149, 3663914278, 76, 79, 115, 75, 2222115970, 76, 1412894473, 79, 148, 235, 32, 1022739065, 66, 81, 81, 81, 81, 33, 81, 82, 901621931, 81, 82, 81, 32, 81, 236, 24, 61, 3060370780, 80, 81, 24, 33, 237, 80, 69, 82, 81, 81, 81, 80, 22, 80, 81, 24, 82, 80, 34, 64, 82, 80, 82, 82, 82, 80, 81, 78, 82, 34, 82, 82, 82, 82, 65, 82, 82, 80, 81, 82, 80, 80, 82, 81, 78, 78, 81, 81, 82, 115, 75, 1701742482, 79, 4091346624, 66, 82, 82, 81, 81, 79, 81, 81, 959153058, 82, 81, 81, 75, 82, 238, 79, 70, 82, 79, 82, 82, 79, 82, 80, 81, 2157832968, 82, 64, 78, 68, 75, 80, 75, 82, 80, 3064829962, 80, 82, 82, 79, 38, 82, 149, 3917335986, 76, 79, 115, 75, 4174241200, 76, 471745885, 79, 148, 239, 35, 2495162703, 68, 36, 80, 24, 80, 81, 1273912613, 81, 80, 80, 35, 36, 81, 61, 2170678781, 81, 81, 24, 36, 240, 81, 69, 82, 80, 80, 81, 81, 22, 81, 82, 24, 80, 82, 37, 64, 80, 81, 80, 82, 80, 80, 81, 78, 80, 37, 81, 82, 82, 81, 65, 82, 82, 80, 80, 82, 81, 80, 82, 81, 78, 78, 81, 82, 80, 115, 75, 3111482293, 79, 149380323, 66, 80, 80, 82, 80, 79, 81, 82, 1729230117, 81, 82, 80, 75, 82, 241, 79, 70, 80, 78, 82, 81, 79, 80, 82, 80, 3064829962, 80, 38, 79, 68, 75, 82, 75, 80, 81, 3583047915, 81, 81, 80, 79, 46, 80, 149, 546553911, 76, 79, 115, 75, 4105996972, 76, 1005657732, 79, 148, 242, 38, 2658516825, 68, 39, 81, 24, 82, 82, 3013512059, 81, 81, 81, 38, 22, 82, 61, 3700557437, 82, 82, 24, 39, 243, 81, 69, 82, 81, 82, 81, 81, 22, 82, 81, 24, 80, 81, 40, 64, 81, 82, 80, 81, 81, 80, 80, 78, 82, 40, 82, 80, 80, 80, 65, 80, 82, 80, 81, 82, 81, 80, 80, 80, 78, 78, 82, 81, 80, 62, 81, 9015, 78, 80, 80, 78, 80, 115, 75, 3456090589, 79, 1549807375, 66, 82, 82, 80, 81, 79, 82, 81, 2538181444, 82, 80, 82, 75, 80, 244, 79, 67, 78, 80, 80, 0, 80, 79, 80, 79, 82, 82, 80, 2, 68, 75, 82, 75, 82, 82, 3970119660, 80, 80, 81, 79, 91, 82, 149, 3426140716, 76, 79, 115, 75, 2886111099, 76, 2516824461, 79, 148, 245, 41, 2409320298, 66, 82, 81, 81, 81, 42, 81, 81, 3330075227, 80, 81, 81, 41, 82, 246, 24, 61, 723634881, 80, 82, 24, 42, 247, 82, 69, 82, 81, 81, 80, 82, 22, 81, 80, 24, 80, 81, 43, 64, 80, 80, 80, 82, 82, 81, 81, 78, 80, 43, 81, 81, 81, 82, 65, 80, 81, 82, 81, 81, 80, 80, 82, 81, 78, 78, 82, 82, 81, 62, 82, 46642, 78, 81, 81, 78, 80, 115, 75, 719862531, 79, 2828814803, 66, 82, 80, 82, 82, 79, 80, 82, 815053529, 80, 82, 82, 75, 80, 248, 79, 67, 78, 80, 82, 0, 82, 79, 82, 79, 81, 80, 81, 1, 68, 75, 80, 75, 81, 81, 2245839489, 80, 80, 80, 79, 31, 81, 149, 3405557230, 76, 79, 115, 75, 59748499, 76, 2817016502, 79, 148, 249, 44, 556327190, 68, 45, 82, 24, 82, 82, 2211313669, 82, 80, 80, 44, 41, 82, 61, 1432817842, 80, 81, 24, 45, 250, 82, 69, 82, 81, 81, 82, 81, 22, 80, 80, 24, 81, 80, 46, 64, 81, 82, 81, 82, 82, 81, 80, 78, 80, 46, 80, 80, 82, 81, 65, 80, 80, 82, 81, 81, 80, 82, 81, 81, 78, 78, 82, 82, 82, 62, 80, 3654, 78, 80, 82, 78, 82, 115, 75, 3482178651, 79, 1484172169, 66, 82, 80, 80, 80, 79, 81, 82, 545660616, 82, 82, 80, 75, 80, 251, 79, 67, 79, 80, 81, 0, 82, 79, 82, 78, 82, 82, 82, 0, 68, 75, 82, 75, 82, 82, 3583047915, 80, 82, 82, 79, 46, 82, 149, 1064678164, 76, 79, 115, 75, 2997347662, 76, 761419916, 79, 148, 252, 47, 3725784502, 68, 48, 81, 24, 81, 80, 2157832968, 82, 82, 81, 47, 64, 81, 61, 2416771862, 80, 81, 24, 48, 253, 80, 69, 81, 80, 81, 80, 80, 22, 81, 81, 24, 81, 80, 49, 64, 82, 81, 80, 82, 82, 82, 81, 78, 82, 49, 80, 81, 80, 82, 65, 81, 81, 81, 82, 82, 82, 82, 80, 80, 78, 78, 82, 82, 80, 115, 75, 1250542183, 79, 895094568, 66, 81, 81, 80, 82, 79, 80, 82, 3041265607, 82, 80, 80, 75, 80, 254, 79, 70, 82, 78, 81, 82, 79, 81, 82, 81, 2072268796, 80, 17, 79, 68, 75, 81, 75, 82, 80, 3970119660, 82, 81, 80, 79, 91, 80, 149, 3024229065, 76, 79, 115, 75, 2347653085, 76, 2315336876, 79, 148, 255, 50, 782594943, 66, 81, 80, 80, 81, 51, 80, 80, 994072187, 80, 80, 81, 50, 81, 256, 24, 61, 3027391095, 80, 80, 24, 51, 257, 81, 69, 81, 80, 80, 80, 80, 22, 81, 81, 24, 80, 80, 52, 64, 81, 80, 82, 82, 80, 81, 82, 78, 81, 52, 81, 81, 82, 82, 65, 81, 81, 82, 81, 82, 81, 82, 80, 80, 78, 78, 81, 81, 80, 62, 82, 63913, 78, 82, 82, 78, 80, 115, 75, 251183662, 79, 2368508286, 66, 81, 80, 81, 80, 79, 80, 80, 1141192656, 81, 80, 82, 75, 82, 258, 79, 67, 79, 81, 81, 1, 82, 79, 80, 78, 80, 82, 81, 2, 68, 75, 80, 75, 82, 80, 2211313669, 81, 80, 80, 79, 41, 81, 149, 1111452054, 76, 79, 115, 75, 3007931796, 76, 1766824254, 79, 148, 259, 53, 2103971184, 68, 54, 82, 24, 81, 82, 3970119660, 81, 82, 81, 53, 91, 82, 61, 3464788917, 81, 81, 24, 54, 260, 80, 69, 81, 81, 82, 82, 81, 22, 82, 81, 24, 82, 82, 55, 64, 80, 80, 82, 80, 82, 80, 81, 78, 82, 55, 82, 82, 80, 80, 65, 80, 82, 82, 82, 82, 82, 80, 81, 81, 78, 78, 82, 81, 80, 62, 81, 58103, 78, 80, 81, 78, 80, 115, 75, 567314992, 79, 2743877472, 66, 80, 82, 80, 82, 79, 80, 80, 2823973160, 82, 82, 81, 75, 82, 261, 79, 67, 79, 82, 82, 1, 80, 79, 81, 78, 81, 82, 81, 2, 68, 75, 82, 75, 81, 81, 1273912613, 81, 81, 81, 79, 36, 82, 149, 2088498730, 76, 79, 115, 75, 3902362529, 76, 3209998490, 79, 148, 262, 56, 177382803, 66, 82, 80, 81, 82, 57, 82, 80, 3008204902, 81, 82, 82, 56, 81, 263, 24, 61, 1888438853, 80, 81, 24, 57, 264, 81, 69, 81, 81, 82, 80, 81, 22, 81, 80, 24, 80, 81, 58, 64, 81, 80, 82, 80, 82, 81, 80, 78, 81, 58, 81, 81, 82, 82, 65, 81, 82, 80, 80, 80, 81, 82, 80, 80, 78, 78, 81, 81, 80, 62, 81, 26647, 78, 80, 82, 78, 80, 115, 75, 3908945554, 79, 1249139142, 66, 81, 80, 80, 81, 79, 82, 82, 3380046536, 82, 82, 80, 75, 80, 265, 79, 67, 79, 80, 82, 1, 81, 79, 80, 78, 81, 80, 80, 1, 68, 75, 81, 75, 81, 80, 2245839489, 80, 81, 80, 79, 31, 80, 149, 3343431046, 76, 79, 115, 75, 4210066496, 76, 3860457522, 79, 148, 266, 59, 753946143, 66, 82, 80, 80, 80, 60, 80, 80, 530948955, 80, 80, 80, 59, 80, 267, 24, 61, 1150333995, 80, 82, 24, 60, 268, 81, 69, 82, 81, 80, 80, 82, 22, 80, 80, 24, 81, 80, 61, 64, 82, 82, 82, 80, 81, 82, 82, 78, 80, 61, 82, 81, 81, 81, 65, 82, 82, 80, 81, 82, 82, 82, 82, 80, 78, 78, 82, 82, 82, 115, 75, 2655966301, 79, 3034786584, 66, 82, 80, 81, 82, 79, 80, 80, 2377401804, 81, 82, 81, 75, 80, 269, 79, 70, 81, 78, 81, 81, 79, 80, 81, 80, 3064829962, 82, 38, 79, 68, 75, 80, 75, 80, 81, 1857505674, 81, 81, 82, 79, 55, 81, 149, 1625413459, 76, 79, 115, 75, 1481017003, 76, 3419094280, 79, 148, 270, 62, 3842059165, 68, 63, 80, 24, 80, 80, 3583047915, 82, 82, 80, 62, 46, 80, 61, 3273483745, 80, 82, 24, 63, 271, 82, 69, 80, 82, 81, 81, 81, 22, 81, 82, 24, 81, 82, 64, 64, 81, 81, 82, 80, 81, 80, 80, 78, 81, 64, 81, 81, 81, 80, 65, 82, 82, 81, 81, 80, 82, 82, 82, 80, 78, 78, 81, 80, 80, 115, 75, 3900752964, 79, 2367180168, 66, 82, 82, 80, 81, 79, 80, 82, 2496534886, 82, 82, 80, 75, 80, 272, 79, 70, 80, 78, 80, 81, 79, 80, 81, 80, 3392015959, 80, 104, 79, 68, 75, 80, 75, 80, 82, 2671826207, 81, 82, 82, 79, 61, 80, 149, 1825450021, 76, 79, 115, 75, 3585934851, 76, 2216590680, 79, 148, 273, 65, 2772862383, 68, 66, 80, 24, 81, 80, 3392015959, 80, 80, 81, 65, 104, 81, 61, 1536961522, 81, 80, 24, 66, 274, 80, 69, 82, 80, 80, 80, 82, 22, 80, 80, 24, 81, 82, 67, 64, 82, 80, 80, 80, 80, 81, 80, 78, 80, 67, 81, 80, 80, 80, 65, 80, 80, 81, 82, 80, 80, 82, 80, 82, 78, 78, 81, 81, 82, 115, 75, 1108467090, 79, 3294440258, 66, 82, 81, 81, 80, 79, 81, 81, 565831871, 80, 82, 82, 75, 81, 275, 79, 70, 82, 78, 80, 80, 79, 80, 80, 81, 2211313669, 82, 41, 79, 68, 75, 81, 75, 80, 81, 3970119660, 82, 81, 80, 79, 91, 82, 149, 3493038161, 76, 79, 115, 75, 209443104, 76, 245122033, 79, 148, 276, 68, 862242298, 66, 80, 82, 81, 82, 69, 82, 82, 1009522515, 82, 82, 82, 68, 80, 277, 24, 61, 1405491528, 81, 81, 24, 69, 278, 80, 69, 81, 82, 82, 80, 81, 22, 81, 81, 24, 82, 81, 70, 64, 80, 81, 81, 81, 80, 80, 80, 78, 82, 70, 82, 80, 81, 81, 65, 80, 81, 80, 80, 81, 80, 80, 80, 81, 78, 78, 80, 81, 82, 115, 75, 1924455900, 79, 4039576332, 66, 80, 80, 82, 81, 79, 82, 82, 564260729, 81, 81, 82, 75, 81, 279, 79, 70, 82, 78, 81, 82, 79, 82, 80, 80, 3013512059, 82, 22, 79, 68, 75, 82, 75, 80, 82, 1523345737, 80, 80, 80, 79, 26, 80, 149, 330250241, 76, 79, 115, 75, 2984129840, 76, 2933887266, 79, 148, 280, 71, 1211920670, 68, 72, 81, 24, 81, 82, 3316678665, 81, 81, 81, 71, 43, 81, 61, 1535430485, 80, 81, 24, 72, 281, 82, 69, 82, 82, 80, 81, 80, 22, 82, 82, 24, 80, 81, 73, 64, 81, 80, 80, 81, 82, 81, 82, 78, 81, 73, 81, 81, 81, 81, 65, 82, 80, 80, 82, 82, 82, 82, 82, 80, 78, 78, 82, 82, 81, 62, 81, 58375, 78, 82, 82, 78, 82, 115, 75, 4023845095, 79, 3381795235, 66, 82, 81, 82, 81, 79, 81, 82, 2865451406, 81, 82, 80, 75, 82, 282, 79, 67, 79, 80, 82, 1, 82, 79, 81, 78, 81, 81, 80, 2, 68, 75, 80, 75, 82, 80, 3013512059, 82, 82, 80, 79, 22, 82, 149, 2653468376, 76, 79, 115, 75, 740307872, 76, 2270651908, 79, 115, 75, 3462623844, 76, 2079609411, 79, 153, 16, 85, 0, 68, 86, 81, 2, 80, 80, 3583047915, 82, 82, 81, 7, 46, 81, 43, 17, 0, 85, 1, 86, 149, 39733, 74, 68, 22, 80, 22, 80, 82, 3583047915, 80, 82, 80, 74, 46, 81, 115, 75, 1186790287, 76, 4079737768, 79, 150, 6, 74, 84, 85, 82, 9799, 0, 82, 82, 11, 80, 1, 81, 82, 82, 82, 75, 85, 80, 81, 1, 82, 82, 12, 82, 82, 120, 13, 283, 2142955284, 74, 6677, 66, 80, 81, 82, 80, 22, 80, 82, 3576614613, 82, 82, 81, 74, 81, 284, 22, 115, 75, 3046258067, 76, 333236, 79, 73, 85, 81, 81, 4269356111, 82, 80, 81, 10, 82, 81, 285, 9, 82, 82, 61, 3903178437, 82, 82, 1, 85, 286, 80, 120, 11, 287, 2687543005, 74, 4790, 68, 22, 82, 22, 81, 80, 145241642, 80, 82, 81, 74, 21, 80, 115, 75, 1248504520, 76, 4294806255, 79, 67, 12, 81, 80, 1, 81, 85, 80, 13, 80, 81, 82, 0, 61, 2805169796, 81, 82, 8, 85, 288, 82, 152, 51, 854373684, 14, 149, 51246, 74, 68, 22, 81, 22, 81, 82, 1523345737, 81, 81, 80, 74, 26, 81, 115, 75, 3864838618, 76, 1405870589, 79, 148, 10, 85, 1121916860, 61, 3610776527, 82, 82, 7, 85, 289, 80, 149, 34163, 74, 66, 81, 80, 82, 81, 22, 81, 80, 2023816856, 80, 81, 81, 74, 82, 290, 22, 115, 75, 3853551296, 76, 1344775911, 79, 148, 291, 85, 3987630683, 68, 9, 82, 1, 80, 80, 3316678665, 80, 80, 80, 85, 43, 80, 120, 10, 10, 1121916860, 74, 17368, 66, 82, 80, 80, 80, 22, 81, 81, 4289604425, 80, 80, 80, 74, 80, 292, 22, 115, 75, 2520549899, 76, 598452780, 79, 127, 85, 7, 0, 85, 61, 4139280639, 82, 82, 85, 7, 293, 82, 148, 5, 85, 3544597836, 68, 19, 82, 7, 82, 82, 1855925594, 80, 82, 81, 85, 125, 81, 149, 3643, 74, 66, 81, 81, 82, 81, 22, 81, 80, 1225428631, 81, 81, 82, 74, 81, 294, 22, 115, 75, 239834991, 76, 3151804232, 79, 140, 85, 210, 4086084229, 15, 14, 85, 16, 199, 4281217317, 74, 44831, 66, 80, 81, 80, 81, 22, 81, 81, 3060915237, 81, 82, 82, 74, 82, 295, 22, 115, 75, 1493891047, 76, 3969767360, 79, 71, 81, 82, 81, 85, 82, 80, 80, 3, 7, 82, 80, 81, 64, 81, 82, 82, 82, 82, 80, 80, 78, 82, 85, 82, 82, 80, 82, 65, 80, 81, 81, 80, 80, 80, 80, 80, 80, 78, 78, 80, 80, 80, 115, 75, 2193854145, 79, 527619986, 66, 81, 82, 82, 80, 79, 80, 80, 2336202986, 82, 80, 81, 75, 82, 296, 79, 70, 82, 79, 80, 82, 79, 81, 81, 82, 1273912613, 82, 36, 78, 68, 75, 82, 75, 82, 81, 3316678665, 82, 80, 82, 79, 43, 81, 149, 968205479, 76, 79, 149, 19000, 74, 68, 22, 81, 22, 81, 82, 2245839489, 81, 81, 81, 74, 31, 80, 115, 75, 3808588399, 76, 1452626504, 79, 149, 20777, 74, 68, 22, 80, 22, 82, 80, 854373684, 82, 82, 82, 74, 51, 82, 115, 75, 1591114655, 76, 3946929080, 79, 126, 5, 85, 207, 1273901866, 61, 1116101317, 82, 82, 6, 85, 297, 80, 149, 15993, 74, 66, 80, 80, 81, 82, 22, 80, 80, 2163434051, 80, 80, 81, 74, 81, 298, 22, 115, 75, 3953715762, 76, 1581238805, 79, 75, 8, 82, 82, 17, 82, 82, 85, 80, 82, 43, 86, 14, 15, 1, 85, 68, 18, 80, 6, 82, 81, 2245839489, 82, 80, 80, 86, 31, 80, 149, 14499, 74, 68, 22, 80, 22, 80, 80, 145241642, 80, 82, 82, 74, 21, 82, 115, 75, 3180582394, 76, 134542813, 79, 61, 2212506049, 81, 81, 7, 19, 299, 80, 149, 53934, 74, 66, 82, 81, 82, 82, 22, 82, 82, 2307117618, 81, 82, 80, 74, 82, 300, 22, 115, 75, 1897399213, 76, 3297715082, 79, 149, 34398, 74, 71, 80, 81, 80, 74, 80, 82, 82, 74, 22, 81, 80, 82, 64, 82, 80, 80, 81, 81, 82, 81, 78, 80, 74, 80, 80, 82, 82, 65, 81, 81, 81, 81, 80, 81, 82, 82, 82, 78, 78, 80, 80, 82, 62, 80, 57862, 78, 81, 82, 78, 82, 115, 75, 4080747031, 79, 2012143047, 66, 82, 81, 82, 81, 79, 82, 81, 1808063371, 81, 81, 81, 75, 82, 301, 79, 67, 78, 82, 82, 0, 81, 79, 80, 79, 81, 80, 82, 0, 68, 75, 81, 75, 81, 81, 1855925594, 81, 80, 82, 79, 125, 81, 149, 1488694675, 76, 79, 149, 13106, 74, 68, 22, 80, 22, 80, 80, 3316678665, 82, 80, 82, 74, 43, 82, 115, 75, 481358322, 76, 2837959125, 79, 149, 0, 74, 68, 22, 82, 22, 82, 82, 3316678665, 81, 80, 82, 74, 43, 82, 115, 75, 3948247515, 76, 1589795324, 79, 61, 3578469873, 82, 82, 6, 18, 302, 80, 149, 4592, 74, 66, 81, 80, 82, 80, 22, 80, 80, 914812771, 82, 80, 82, 74, 82, 303, 22, 115, 75, 1066492963, 76, 2315666436, 79, 42, 75, 77, 2, 76, 75, 114, 129, 9, 10, 1121916860, 8, 304, 3006071662, 6, 305, 4246247999, 7, 306, 3811857247, 4, 148, 307, 10, 46427574, 72, 2, 6, 6, 7, 6, 7, 6, 11, 10, 6, 7, 73, 10, 6, 6, 665239807, 6, 7, 7, 11, 6, 6, 308, 1, 7, 6, 148, 10, 11, 1121916860, 74, 9, 12, 6, 10616, 2, 7, 7, 11, 7, 10, 7, 6, 7, 7, 61, 2163799957, 7, 7, 5, 12, 309, 7, 148, 310, 10, 2168733779, 68, 11, 6, 0, 7, 6, 2211313669, 7, 6, 6, 10, 41, 7, 148, 10, 10, 1121916860, 74, 9, 12, 7, 10660, 1, 7, 7, 10, 7, 11, 7, 7, 6, 7, 61, 3986979059, 6, 6, 0, 12, 311, 6, 76, 7, 0, 7, 6, 7, 10, 6, 7, 6, 7, 6, 148, 10, 11, 1121916860, 74, 9, 12, 7, 10699, 1, 6, 7, 11, 6, 10, 6, 6, 6, 6, 61, 36048799, 6, 7, 0, 12, 312, 7, 148, 313, 10, 2179748059, 74, 9, 11, 6, 10726, 0, 6, 6, 10, 7, 0, 6, 7, 6, 6, 75, 11, 6, 7, 0, 6, 7, 10, 7, 6, 148, 10, 11, 1121916860, 74, 9, 12, 7, 10755, 1, 7, 6, 11, 7, 10, 6, 7, 7, 7, 61, 2233767628, 7, 6, 0, 12, 314, 7, 68, 10, 7, 0, 7, 7, 854373684, 6, 6, 7, 5, 51, 7, 148, 10, 11, 1121916860, 74, 9, 12, 7, 10795, 2, 7, 7, 11, 6, 10, 6, 6, 6, 7, 61, 2706336138, 7, 6, 0, 12, 315, 6, 76, 6, 0, 6, 6, 6, 10, 6, 6, 6, 6, 6, 148, 10, 11, 1121916860, 74, 9, 12, 7, 10834, 0, 6, 6, 11, 6, 10, 6, 6, 6, 6, 61, 627124695, 6, 6, 0, 12, 316, 7, 130, 10, 83, 3608460499, 11, 317, 3551961597, 12, 10, 11, 13, 318, 2128282211, 14, 10, 12, 2, 0, 13, 148, 10, 10, 1121916860, 74, 9, 11, 6, 10880, 2, 7, 6, 10, 7, 14, 7, 7, 7, 6, 61, 2700340810, 7, 7, 0, 11, 319, 6, 148, 320, 10, 1750453714, 75, 10, 7, 7, 0, 7, 6, 11, 7, 7, 148, 10, 10, 1121916860, 74, 9, 12, 6, 10921, 0, 7, 7, 10, 6, 11, 6, 6, 7, 7, 61, 1526246875, 6, 7, 0, 12, 321, 6, 144, 10, 127, 807505853, 11, 1, 10, 0, 11, 127, 10, 7, 0, 10, 128, 6, 322, 1914740530, 10, 10, 1121916860, 8, 323, 3854639981, 7, 324, 374488047, 9, 325, 1278259109, 126, 4, 11, 307, 46427574, 72, 2, 8, 7, 6, 7, 8, 8, 12, 11, 7, 8, 73, 11, 8, 8, 3489516062, 8, 7, 8, 12, 8, 8, 326, 1, 6, 8, 148, 10, 12, 1121916860, 74, 10, 13, 8, 11010, 2, 7, 8, 12, 8, 11, 8, 8, 8, 6, 61, 3784871238, 6, 6, 5, 13, 327, 8, 148, 328, 11, 3414452207, 74, 10, 12, 8, 11037, 0, 8, 8, 11, 6, 0, 7, 6, 7, 7, 75, 12, 8, 8, 0, 8, 8, 11, 6, 6, 148, 10, 12, 1121916860, 74, 10, 13, 6, 11066, 1, 6, 6, 12, 6, 11, 7, 6, 7, 7, 61, 788449823, 6, 7, 0, 13, 329, 7, 148, 330, 11, 1503456473, 66, 7, 7, 7, 7, 12, 7, 7, 265772243, 7, 7, 7, 11, 6, 331, 0, 148, 10, 11, 1121916860, 74, 10, 13, 6, 11113, 1, 6, 8, 11, 7, 12, 7, 8, 7, 7, 61, 2331738068, 6, 7, 0, 13, 332, 6, 75, 5, 6, 7, 0, 8, 8, 11, 8, 6, 148, 10, 12, 1121916860, 74, 10, 13, 7, 11150, 0, 8, 7, 12, 7, 11, 8, 8, 7, 7, 61, 1353357371, 7, 6, 0, 13, 333, 7, 148, 334, 11, 2050527430, 68, 12, 7, 0, 7, 8, 3583047915, 7, 7, 7, 11, 46, 7, 148, 10, 11, 1121916860, 74, 10, 13, 7, 11194, 2, 8, 6, 11, 7, 12, 7, 6, 8, 8, 61, 2734260421, 6, 8, 0, 13, 335, 8, 130, 11, 83, 3608460499, 12, 317, 3551961597, 13, 11, 12, 14, 336, 1214481746, 15, 11, 13, 2, 0, 14, 148, 10, 11, 1121916860, 74, 10, 12, 7, 11240, 1, 8, 7, 11, 7, 15, 7, 8, 6, 7, 61, 1143823618, 6, 7, 0, 12, 337, 7, 148, 338, 11, 2713150735, 75, 11, 8, 7, 0, 7, 7, 12, 6, 7, 148, 10, 11, 1121916860, 74, 10, 13, 6, 11281, 1, 8, 6, 11, 6, 12, 8, 6, 6, 8, 61, 1710905293, 7, 6, 0, 13, 339, 7, 144, 11, 218, 2817017979, 12, 1, 11, 0, 12, 127, 11, 7, 0, 11, 128, 7, 340, 3203506559, 8, 10, 1121916860, 5, 341, 2982291194, 4, 342, 1414180189, 6, 343, 1470747600, 151, 3, 75, 0, 4, 5, 1, 5, 5, 9, 5, 5, 148, 10, 10, 1121916860, 74, 8, 11, 4, 11350, 1, 4, 5, 10, 5, 9, 6, 6, 5, 5, 61, 2640530937, 6, 5, 1, 11, 344, 5, 148, 345, 9, 2817354145, 74, 8, 10, 6, 11377, 0, 6, 6, 9, 5, 1, 6, 4, 5, 5, 75, 10, 4, 5, 1, 4, 4, 9, 5, 6, 148, 10, 10, 1121916860, 74, 8, 11, 6, 11406, 1, 6, 5, 10, 6, 9, 4, 6, 6, 6, 61, 934171557, 6, 6, 1, 11, 346, 4, 130, 9, 83, 3608460499, 10, 317, 3551961597, 11, 9, 10, 12, 347, 3744265389, 13, 9, 11, 2, 1, 12, 148, 10, 9, 1121916860, 74, 8, 10, 6, 11452, 0, 4, 6, 9, 5, 13, 5, 5, 6, 6, 61, 3785331601, 5, 6, 1, 10, 348, 5, 76, 5, 1, 4, 5, 5, 9, 5, 5, 6, 5, 5, 148, 10, 10, 1121916860, 74, 8, 11, 6, 11491, 1, 5, 5, 10, 5, 9, 5, 5, 5, 5, 61, 733092453, 4, 4, 1, 11, 349, 4, 148, 345, 9, 2817354145, 72, 1, 5, 4, 4, 6, 4, 6, 10, 9, 6, 5, 148, 350, 9, 3813580087, 74, 8, 11, 5, 11534, 0, 6, 4, 9, 4, 1, 6, 4, 5, 5, 73, 9, 6, 5, 4184152874, 6, 4, 4, 11, 6, 6, 351, 10, 6, 4, 148, 10, 10, 1121916860, 74, 8, 11, 5, 11568, 2, 5, 6, 10, 6, 9, 5, 6, 5, 5, 61, 2622140103, 6, 5, 1, 11, 352, 6, 148, 353, 9, 410075897, 75, 9, 6, 6, 1, 6, 4, 10, 6, 6, 148, 10, 9, 1121916860, 74, 8, 11, 6, 11609, 0, 6, 6, 9, 6, 10, 5, 6, 6, 6, 61, 1416548930, 4, 6, 1, 11, 354, 6, 150, 1, 127, 9, 7, 0, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 19; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case 98: + { + // TRY_SETUP_3959_3 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 3959, + exceptionReg: 3 + }); + break; + } + case 119: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 75: + { + // ANTI_BXOR_8_2_1_7_4_0_6_3_5 (order: [8,2,1,7,4,0,6,3,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[0]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // MOD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a % regs[base + _operands[5]]; + // BXOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a ^ regs[base + _operands[8]]; + break; + } + case 113: + { + // JUMP_REG_70 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 70]; + break; + } + case 83: + { + // LOAD_CONST_12_0_1911865391 (specialized) + var dst = 12; + regs[base + dst] = this._constant(0, 1911865391); + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 140: + { + // LOAD_CONST,GET_PROP,LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case 88: + { + // LOAD_INT_9_3310329788 (specialized) + var dst = 9; + regs[base + dst] = 3310329788; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case 146: + { + // LOAD_CONST,BUILD_OBJECT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 85: + { + // LOAD_CONST_7_2_1127611587 (specialized) + var dst = 7; + regs[base + dst] = this._constant(2, 1127611587); + break; + } + case 84: + { + // LOAD_CONST_13_1_1875186215 (specialized) + var dst = 13; + regs[base + dst] = this._constant(1, 1875186215); + break; + } + case 104: + { + // LOAD_UPVALUE_14_0 (specialized) + var dst = 14; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 79: + { + // JUMP_10545 (specialized) + regs[fp + SLOTS.PC] = 10545; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case 76: + { + // ANTI_UNARY_BITNOT_2_7_10_0_4_6_8_9_1_3_5 (order: [2,7,10,0,4,6,8,9,1,3,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[2]]; + // SHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >> regs[base + _operands[2]]; + // LOOSE_EQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a == regs[base + _operands[5]]; + // UNARY_BITNOT + var dst = _operands[6]; + regs[base + dst] = ~regs[base + _operands[7]]; + // NEQ + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a !== regs[base + _operands[10]]; + break; + } + case 67: + { + // ANTI_BAND_1_5_10_4_3_0_9_2_11_8_6_7 (order: [1,5,10,4,3,0,9,2,11,8,6,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[11], _unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[8]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[3]; + var count = _operands[4]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[5]]; + regs[base + dst] = elems; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[6]; + var count = _operands[7]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[8]]; + regs[base + dst] = elems; + // LOOSE_EQ + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a == regs[base + _operands[11]]; + break; + } + case 148: + { + // ALIAS_LOAD_CONST_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 150: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 87: + { + // LOAD_GLOBAL_14_4_1207012017 (specialized) + var dst = 14; + var globalName = this._constant(4, 1207012017); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case 100: + { + // GET_PROP_106_3_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 106; + var obj = regs[base + 3]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; + break; + } + case 78: + { + // JUMP_7502 (specialized) + regs[fp + SLOTS.PC] = 7502; + break; + } + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 120: + { + // LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case 121: + { + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 91: + { + // LOAD_GLOBAL_16_4_1207012017 (specialized) + var dst = 16; + var globalName = this._constant(4, 1207012017); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + case 102: + { + // LOAD_GLOBAL_7_125_1855925594 (specialized) + var dst = 7; + var globalName = this._constant(125, 1855925594); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case 70: + { + // ANTI_MUL_5_11_4_3_9_0_6_2_8_1_7_10 (order: [5,11,4,3,9,0,6,2,8,1,7,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[1]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // BAND + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a & regs[base + _operands[5]]; + // LOAD_GLOBAL + var dst = _operands[6]; + var globalName = this._constant(_operands[7], _operands[8]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // MUL + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a * regs[base + _operands[11]]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 99: + { + // THROW_5 (specialized) + throw regs[base + 5]; + break; + } + case 126: + { + // LOAD_THIS,LOAD_CONST (macro) + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 64: + { + // ANTI_UNARY_NOT_1_4_10_5_13_11_8_6_9_7_0_2_3_12 (order: [1,4,10,5,13,11,8,6,9,7,0,2,3,12]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[13], _unsortedOperands[4]]; + // MOD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a % regs[base + _operands[2]]; + // MOD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a % regs[base + _operands[5]]; + // UNARY_NOT + var dst = _operands[6]; + regs[base + dst] = !regs[base + _operands[7]]; + // SHR + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a >> regs[base + _operands[10]]; + // NEQ + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a !== regs[base + _operands[13]]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case 138: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 125: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 143: + { + // LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case 132: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 62: + { + // ANTI_UNARY_NEG_2_1_5_3_4_6_0 (order: [2,1,5,3,4,6,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[5]]; + // LOAD_INT + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + // LOOSE_NEQ + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a != regs[base + _operands[4]]; + // UNARY_NEG + var dst = _operands[5]; + regs[base + dst] = -regs[base + _operands[6]]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 82: + { + // LOAD_INT_18_8 (specialized) + var dst = 18; + regs[base + dst] = 8; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case 95: + { + // LOAD_THIS_19 (specialized) + var dst = 19; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 147: + { + // BUILD_OBJECT,RETURN (macro) + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 139: + { + // LOAD_INT,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 96: + { + // RETURN_106 (specialized) + var retVal = regs[base + 106]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 130: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 128: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case 68: + { + // ANTI_ADD_0_8_1_10_3_5_9_11_6_2_4_7 (order: [0,8,1,10,3,5,9,11,6,2,4,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[11], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[7]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // LOAD_GLOBAL + var dst = _operands[3]; + var globalName = this._constant(_operands[4], _operands[5]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // NEQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a !== regs[base + _operands[8]]; + // DIV + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a / regs[base + _operands[11]]; + break; + } + case 103: + { + // THROW_106 (specialized) + throw regs[base + 106]; + break; + } + case 108: + { + // GET_PROP_95_95_99 (specialized) + // dst = regs[obj][regs[key]] + var dst = 95; + var obj = regs[base + 95]; + var key = regs[base + 99]; + regs[base + dst] = obj[key]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case 134: + { + // LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 135: + { + // GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 116: + { + // LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 63: + { + // ANTI_NEQ_1_7_2_6_4_5_0_3 (order: [1,7,2,6,4,5,0,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[1]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // JUMP_IF_FALSE + var src = _operands[3]; + var target = _operands[4]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // NEQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a !== regs[base + _operands[7]]; + break; + } + case 152: + { + // ALIAS_LOAD_GLOBAL_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 133: + { + // LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case 66: + { + // ANTI_SUB_9_14_13_2_3_0_1_8_6_10_11_5_12_7_4 (order: [9,14,13,2,3,0,1,8,6,10,11,5,12,7,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[14], _unsortedOperands[11], _unsortedOperands[8], _unsortedOperands[13], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[12], _unsortedOperands[2], _unsortedOperands[1]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // SUB + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a - regs[base + _operands[5]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[6]; + var name = this._constant(_operands[7], _operands[8]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // LT + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a < regs[base + _operands[11]]; + // LT + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a < regs[base + _operands[14]]; + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case 127: + { + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 93: + { + // SET_PROP_14_3_16 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 14]; + var key = regs[base + 3]; + var val = regs[base + 16]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 137: + { + // LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 118: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case 141: + { + // LOAD_CONST,LOAD_CONST,LOAD_THIS,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 106: + { + // SET_PROP_11_12_13 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 11]; + var key = regs[base + 12]; + var val = regs[base + 13]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case 151: + { + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 92: + { + // GET_PROP_18_16_4 (specialized) + // dst = regs[obj][regs[key]] + var dst = 18; + var obj = regs[base + 16]; + var key = regs[base + 4]; + regs[base + dst] = obj[key]; + break; + } + case 145: + { + // LOAD_INT,CALL (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // CALL + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + case 65: + { + // ANTI_UNARY_POS_10_8_11_12_4_9_2_7_3_0_1_13_5_6 (order: [10,8,11,12,4,9,2,7,3,0,1,13,5,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[12], _unsortedOperands[13], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[11]]; + // UNARY_POS + var dst = _operands[0]; + regs[base + dst] = +regs[base + _operands[1]]; + // DIV + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a / regs[base + _operands[4]]; + // GTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a >= regs[base + _operands[7]]; + // SHL + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a << regs[base + _operands[10]]; + // LT + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a < regs[base + _operands[13]]; + break; + } + case 131: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case 114: + { + // JUMP_REG_75 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 75]; + break; + } + case 69: + { + // ANTI_EQ_10_1_0_6_8_3_9_11_4_7_5_2 (order: [10,1,0,6,8,3,9,11,4,7,5,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[7]]; + // TYPEOF + var dst = _operands[0]; + regs[base + dst] = typeof regs[base + _operands[1]]; + // EQ + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a === regs[base + _operands[4]]; + // UNARY_NOT + var dst = _operands[5]; + regs[base + dst] = !regs[base + _operands[6]]; + // GT + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a > regs[base + _operands[9]]; + // MOVE + var dst = _operands[10]; + regs[base + dst] = regs[base + _operands[11]]; + break; + } + case 80: + { + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 110: + { + // LOAD_THIS_15 (specialized) + var dst = 15; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 117: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case 81: + { + // LOAD_INT_94_3751 (specialized) + var dst = 94; + regs[base + dst] = 3751; + break; + } + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case 109: + { + // JUMP_REG_95 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 95]; + break; + } + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case 77: + { + // JUMP_4417 (specialized) + regs[fp + SLOTS.PC] = 4417; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + case 61: + { + // ANTI_MOVE_4_0_1_5_6_3_2 (order: [4,0,1,5,6,3,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[4]]; + // UNARY_NOT + var dst = _operands[0]; + regs[base + dst] = !regs[base + _operands[1]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[2]; + var name = this._constant(_operands[3], _operands[4]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // MOVE + var dst = _operands[5]; + regs[base + dst] = regs[base + _operands[6]]; + break; + } + case 136: + { + // LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 101: + { + // LOAD_GLOBAL_11_4_1207012017 (specialized) + var dst = 11; + var globalName = this._constant(4, 1207012017); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case 97: + { + // FINALLY_SETUP_4217_4_5_3992 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 4217, + contReg: 4, + payloadReg: 5, + throwPad: 3992 + }); + break; + } + case 112: + { + // RETURN_81 (specialized) + var retVal = regs[base + 81]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 86: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case 129: + { + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 144: + { + // LOAD_CONST,BUILD_OBJECT,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 72: + { + // ANTI_SHL_1_10_7_5_9_3_8_0_2_6_4 (order: [1,10,7,5,9,3,8,0,2,6,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[1]]; + // SHL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a << regs[base + _operands[2]]; + // BXOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a ^ regs[base + _operands[5]]; + // UNARY_NOT + var dst = _operands[6]; + regs[base + dst] = !regs[base + _operands[7]]; + // ADD + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a + regs[base + _operands[10]]; + break; + } + case 122: + { + // LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 107: + { + // JUMP_REG_4 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 4]; + break; + } + case 89: + { + // LOAD_INT_10_113 (specialized) + var dst = 10; + regs[base + dst] = 113; + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 149: + { + // ALIAS_LOAD_INT_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + break; + } + case 74: + { + // ANTI_USHR_6_3_2_7_12_13_9_5_1_4_11_0_8_10 (order: [6,3,2,7,12,13,9,5,1,4,11,0,8,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[11], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[12], _unsortedOperands[6], _unsortedOperands[13], _unsortedOperands[10], _unsortedOperands[4], _unsortedOperands[5]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // USHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >>> regs[base + _operands[5]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[6]; + var target = _operands[7]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // LOOSE_EQ + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a == regs[base + _operands[10]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[11]; + var count = _operands[12]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[13]]; + regs[base + dst] = elems; + break; + } + case 153: + { + // ALIAS_GET_PROP_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + // dst = regs[obj][regs[key]] + var dst = _operands[0]; + var obj = regs[base + _operands[1]]; + var key = regs[base + _operands[2]]; + regs[base + dst] = obj[key]; + break; + } + case 73: + { + // ANTI_BOR_3_8_6_11_12_1_9_5_7_13_10_4_0_2 (order: [3,8,6,11,12,1,9,5,7,13,10,4,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[12], _unsortedOperands[5], _unsortedOperands[13], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[9]]; + // LTE + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a <= regs[base + _operands[2]]; + // BOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a | regs[base + _operands[5]]; + // DIV + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a / regs[base + _operands[8]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[9]; + var name = this._constant(_operands[10], _operands[11]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // UNARY_BITNOT + var dst = _operands[12]; + regs[base + dst] = ~regs[base + _operands[13]]; + break; + } + case 123: + { + // GET_PROP,LOAD_CONST (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + case 115: + { + // LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 105: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + case 94: + { + // RETURN_14 (specialized) + var retVal = regs[base + 14]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 142: + { + // GET_PROP,LOAD_CONST,LOAD_INT (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case 111: + { + // GET_PROP_82_3_81 (specialized) + // dst = regs[obj][regs[key]] + var dst = 82; + var obj = regs[base + 3]; + var key = regs[base + 81]; + regs[base + dst] = obj[key]; + break; + } + case 124: + { + // LOAD_GLOBAL,LOAD_CONST,GET_PROP (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case 71: + { + // ANTI_LT_6_9_11_0_7_5_3_2_1_10_4_8 (order: [6,9,11,0,7,5,3,2,1,10,4,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[2]]; + // LT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // MOD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a % regs[base + _operands[8]]; + // EQ + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a === regs[base + _operands[11]]; + break; + } + case 90: + { + // LOAD_CONST_15_5_3544597836 (specialized) + var dst = 15; + regs[base + dst] = this._constant(5, 3544597836); + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/qualification.json new file mode 100644 index 00000000..3e70c435 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/qualification.json @@ -0,0 +1,80 @@ +{ + "id": "exceptions--nineOptionAliasShuffleStack--118", + "case": "exceptions", + "profile": "nineOptionAliasShuffleStack", + "seed": 118, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": true, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": true, + "specializedOpcodes": true, + "aliasedOpcodes": true, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "emitted-representation-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true, + "concealConstants": true, + "antiInstrumentation": true, + "specializedOpcodes": true, + "macroOpcodes": true, + "aliasedOpcodes": true, + "shuffleOpcodes": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/shuffled-alias-stack-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/encoded.js b/test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/encoded.js new file mode 100644 index 00000000..697b93e3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/encoded.js @@ -0,0 +1,1334 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"id", /* 3 */7, /* 4 */undefined, /* 5 */":"]; +var BYTECODE = [61, 47, 3, 30, 1, 4, 0, 0, 62, 63, 64, 65, 42, 6, 2, 1, 5, 66, 42, 7, 6, 1, 5, 42, 5, 7, 0, 67, 68, 69, 70, 47, 3, 46, 1, 4, 1, 0, 1, 0, 71, 0, 3, 4, 0, 72, 73, 47, 3, 64, 0, 5, 2, 0, 0, 0, 1, 0, 74, 80, 3, 4, 0, 3, 75, 76, 0, 3, 5, 0, 77, 78, 79, 45, 3, 80, 2, 4, 0, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 62: + { + // MOVE_2_3 (specialized) + var dst = 2; + regs[base + dst] = regs[base + 3]; + break; + } + case 63: + { + // LOAD_GLOBAL_3_0_0 (specialized) + var dst = 3; + var globalName = this._constant(0, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 64: + { + // LOAD_CONST_4_1_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(1, 0); + break; + } + case 65: + { + // LOAD_CONST_5_2_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(2, 0); + break; + } + case 66: + { + // LOAD_CONST_5_3_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(3, 0); + break; + } + case 67: + { + // SET_PROP_3_4_5 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 3]; + var key = regs[base + 4]; + var val = regs[base + 5]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 68: + { + // LOAD_CONST_3_4_0 (specialized) + var dst = 3; + regs[base + dst] = this._constant(4, 0); + break; + } + case 69: + { + // RETURN_3 (specialized) + var retVal = regs[base + 3]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 70: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 71: + { + // RETURN_3 (specialized) + var retVal = regs[base + 3]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 72: + { + // RETURN_3 (specialized) + var retVal = regs[base + 3]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 73: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 74: + { + // RETURN_3 (specialized) + var retVal = regs[base + 3]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 75: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 76: + { + // LOAD_UPVALUE_2_0 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 77: + { + // ADD_4_2_3 (specialized) + var dst = 4; + var a = regs[base + 2]; + regs[base + dst] = a + regs[base + 3]; + break; + } + case 78: + { + // LOAD_UPVALUE_2_1 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[1]._read(); + break; + } + case 79: + { + // ADD_3_4_2 (specialized) + var dst = 3; + var a = regs[base + 4]; + regs[base + dst] = a + regs[base + 2]; + break; + } + case 80: + { + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/qualification.json b/test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/qualification.json new file mode 100644 index 00000000..0ade8596 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/qualification.json @@ -0,0 +1,61 @@ +{ + "id": "nestedStrings--specializedOpcodesMacroOpcodes--98", + "case": "nestedStrings", + "profile": "specializedOpcodesMacroOpcodes", + "seed": 98, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": true, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": "id:7" + }, + "encodedObservation": { + "kind": "value", + "value": "id:7" + }, + "decodedObservation": { + "kind": "value", + "value": "id:7" + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "specializedOpcodes": true, + "macroOpcodes": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/source.js b/test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/source.js new file mode 100644 index 00000000..fbfe36eb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-macro-nested-strings/source.js @@ -0,0 +1 @@ +function outer(prefix) { return function middle(n) { return function inner() { return prefix + ":" + n; }; }; } window.TEST_OUTPUT = outer("id")(7)(); diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/encoded.js b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/encoded.js new file mode 100644 index 00000000..a1dfa1b4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/encoded.js @@ -0,0 +1,1140 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = [61, 62, 63, 64, 65, 66, 67, 68, 0, 4, 5, 0, 69, 70, 0, 2, 6, 0, 71]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 62: + { + // LOAD_GLOBAL_2_0_0 (specialized) + var dst = 2; + var globalName = this._constant(0, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 63: + { + // LOAD_CONST_3_1_0 (specialized) + var dst = 3; + regs[base + dst] = this._constant(1, 0); + break; + } + case 64: + { + // LOAD_CONST_4_2_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(2, 0); + break; + } + case 65: + { + // LOAD_CONST_5_3_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(3, 0); + break; + } + case 66: + { + // MUL_6_4_5 (specialized) + var dst = 6; + var a = regs[base + 4]; + regs[base + dst] = a * regs[base + 5]; + break; + } + case 67: + { + // LOAD_CONST_4_4_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(4, 0); + break; + } + case 68: + { + // ADD_5_6_4 (specialized) + var dst = 5; + var a = regs[base + 6]; + regs[base + dst] = a + regs[base + 4]; + break; + } + case 69: + { + // MOD_6_5_4 (specialized) + var dst = 6; + var a = regs[base + 5]; + regs[base + dst] = a % regs[base + 4]; + break; + } + case 70: + { + // SET_PROP_2_3_6 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 2]; + var key = regs[base + 3]; + var val = regs[base + 6]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 71: + { + // RETURN_2 (specialized) + var retVal = regs[base + 2]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/qualification.json b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/qualification.json new file mode 100644 index 00000000..baae43e9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "arithmetic--specializedOpcodes--22", + "case": "arithmetic", + "profile": "specializedOpcodes", + "seed": 22, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": { + "kind": "value", + "value": 0 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/source.js b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-boot/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/encoded.js b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/encoded.js new file mode 100644 index 00000000..473f0840 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/encoded.js @@ -0,0 +1,1212 @@ +var CONSTANTS = [/* 0 */0, /* 1 */4, /* 2 */2, /* 3 */1, /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */undefined]; +var BYTECODE = [61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 5, 4, 3, 0, 4, 3, 0, 77, 5, 3, 5, 78, 79, 0, 5, 5, 0, 80, 0, 4, 6, 0, 81]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 6; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 62: + { + // LOAD_CONST_4_0_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(0, 0); + break; + } + case 63: + { + // MOVE_2_4 (specialized) + var dst = 2; + regs[base + dst] = regs[base + 4]; + break; + } + case 64: + { + // LOAD_CONST_4_0_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(0, 0); + break; + } + case 65: + { + // MOVE_3_4 (specialized) + var dst = 3; + regs[base + dst] = regs[base + 4]; + break; + } + case 66: + { + // LOAD_CONST_4_1_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(1, 0); + break; + } + case 67: + { + // LT_5_3_4 (specialized) + var dst = 5; + var a = regs[base + 3]; + regs[base + dst] = a < regs[base + 4]; + break; + } + case 68: + { + // JUMP_IF_FALSE_5_28 (specialized) + var src = 5; + var target = 28; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 69: + { + // LOAD_CONST_4_2_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(2, 0); + break; + } + case 70: + { + // MOD_5_3_4 (specialized) + var dst = 5; + var a = regs[base + 3]; + regs[base + dst] = a % regs[base + 4]; + break; + } + case 71: + { + // JUMP_IF_FALSE_5_14 (specialized) + var src = 5; + var target = 14; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 72: + { + // ADD_4_2_3 (specialized) + var dst = 4; + var a = regs[base + 2]; + regs[base + dst] = a + regs[base + 3]; + break; + } + case 73: + { + // MOVE_2_4 (specialized) + var dst = 2; + regs[base + dst] = regs[base + 4]; + break; + } + case 74: + { + // JUMP_16 (specialized) + regs[fp + SLOTS.PC] = 16; + break; + } + case 75: + { + // SUB_4_2_3 (specialized) + var dst = 4; + var a = regs[base + 2]; + regs[base + dst] = a - regs[base + 3]; + break; + } + case 76: + { + // MOVE_2_4 (specialized) + var dst = 2; + regs[base + dst] = regs[base + 4]; + break; + } + case 77: + { + // ADD_5_3_4 (specialized) + var dst = 5; + var a = regs[base + 3]; + regs[base + dst] = a + regs[base + 4]; + break; + } + case 78: + { + // JUMP_5 (specialized) + regs[fp + SLOTS.PC] = 5; + break; + } + case 79: + { + // LOAD_GLOBAL_4_4_0 (specialized) + var dst = 4; + var globalName = this._constant(4, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 80: + { + // SET_PROP_4_5_2 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 4]; + var key = regs[base + 5]; + var val = regs[base + 2]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 81: + { + // RETURN_4 (specialized) + var retVal = regs[base + 4]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/qualification.json b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/qualification.json new file mode 100644 index 00000000..16845a46 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "branches--specializedOpcodes--23", + "case": "branches", + "profile": "specializedOpcodes", + "seed": 23, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 2 + }, + "encodedObservation": { + "kind": "value", + "value": 2 + }, + "decodedObservation": { + "kind": "value", + "value": 2 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/source.js b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/source.js new file mode 100644 index 00000000..a2f57916 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-branches/source.js @@ -0,0 +1 @@ +let value = 0; for (let i = 0; i < 4; i++) { if (i % 2) value += i; else value -= i; } window.TEST_OUTPUT = value; diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/encoded.js b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/encoded.js new file mode 100644 index 00000000..f05dcbde --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/encoded.js @@ -0,0 +1,1283 @@ +var CONSTANTS = [/* 0 */3, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */undefined, /* 4 */1]; +var BYTECODE = [61, 47, 4, 34, 1, 5, 0, 0, 62, 63, 42, 5, 2, 1, 4, 64, 65, 66, 42, 6, 3, 0, 42, 7, 3, 0, 48, 8, 2, 6, 7, 67, 68, 69, 70, 71, 47, 4, 48, 0, 3, 1, 0, 1, 3, 72, 73, 74, 75, 0, 1, 4, 0, 76, 77, 78]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 62: + { + // MOVE_2_4 (specialized) + var dst = 2; + regs[base + dst] = regs[base + 4]; + break; + } + case 63: + { + // LOAD_CONST_4_0_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(0, 0); + break; + } + case 64: + { + // MOVE_3_5 (specialized) + var dst = 3; + regs[base + dst] = regs[base + 5]; + break; + } + case 65: + { + // LOAD_GLOBAL_4_1_0 (specialized) + var dst = 4; + var globalName = this._constant(1, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 66: + { + // LOAD_CONST_5_2_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(2, 0); + break; + } + case 67: + { + // SET_PROP_4_5_8 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 4]; + var key = regs[base + 5]; + var val = regs[base + 8]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 68: + { + // LOAD_CONST_4_3_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(3, 0); + break; + } + case 69: + { + // RETURN_4 (specialized) + var retVal = regs[base + 4]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 70: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 71: + { + // MOVE_3_0 (specialized) + var dst = 3; + regs[base + dst] = regs[base + 0]; + break; + } + case 72: + { + // RETURN_4 (specialized) + var retVal = regs[base + 4]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 73: + { + // LOAD_CONST_4_3_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(3, 0); + break; + } + case 74: + { + // RETURN_4 (specialized) + var retVal = regs[base + 4]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 75: + { + // LOAD_UPVALUE_0_0 (specialized) + var dst = 0; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 76: + { + // ADD_2_0_1 (specialized) + var dst = 2; + var a = regs[base + 0]; + regs[base + dst] = a + regs[base + 1]; + break; + } + case 77: + { + // STORE_UPVALUE_0_2 (specialized) + var uvIdx = 0; + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + 2]); + break; + } + case 78: + { + // RETURN_2 (specialized) + var retVal = regs[base + 2]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/qualification.json b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/qualification.json new file mode 100644 index 00000000..67dbae54 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "closures--specializedOpcodes--24", + "case": "closures", + "profile": "specializedOpcodes", + "seed": 24, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/source.js b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/source.js new file mode 100644 index 00000000..8148a2c5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-closures/source.js @@ -0,0 +1 @@ +function counter(start) { let value = start; return () => ++value; } const next = counter(3); window.TEST_OUTPUT = [next(), next()]; diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/encoded.js new file mode 100644 index 00000000..366fdf8a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/encoded.js @@ -0,0 +1,1401 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */"finished", /* 5 */undefined, /* 6 */0, /* 7 */"Error", /* 8 */"negative", /* 9 */"message", /* 10 */true]; +var BYTECODE = [63, 47, 3, 39, 1, 9, 0, 0, 64, 65, 66, 67, 68, 42, 5, 2, 1, 6, 69, 42, 7, 2, 1, 6, 70, 71, 72, 48, 6, 3, 5, 7, 9, 73, 0, 3, 5, 0, 74, 75, 76, 77, 0, 6, 6, 0, 78, 79, 80, 0, 7, 8, 0, 44, 8, 6, 1, 7, 81, 0, 6, 2, 0, 82, 55, 83, 84, 55, 61, 55, 85, 0, 6, 9, 0, 86, 87, 88, 55, 61, 55, 89, 61, 90, 0, 7, 4, 0, 0, 8, 10, 0, 91, 92, 93, 62, 62, 0, 6, 5, 0, 94]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // JUMP_83 (specialized) + regs[fp + SLOTS.PC] = 83; + break; + } + case 62: + { + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 63: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 64: + { + // MOVE_2_3 (specialized) + var dst = 2; + regs[base + dst] = regs[base + 3]; + break; + } + case 65: + { + // LOAD_GLOBAL_3_0_0 (specialized) + var dst = 3; + var globalName = this._constant(0, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 66: + { + // LOAD_CONST_4_1_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(1, 0); + break; + } + case 67: + { + // LOAD_CONST_5_2_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(2, 0); + break; + } + case 68: + { + // UNARY_NEG_6_5 (specialized) + var dst = 6; + regs[base + dst] = -regs[base + 5]; + break; + } + case 69: + { + // LOAD_CONST_6_3_0 (specialized) + var dst = 6; + regs[base + dst] = this._constant(3, 0); + break; + } + case 70: + { + // LOAD_GLOBAL_6_0_0 (specialized) + var dst = 6; + var globalName = this._constant(0, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 71: + { + // LOAD_CONST_8_4_0 (specialized) + var dst = 8; + regs[base + dst] = this._constant(4, 0); + break; + } + case 72: + { + // GET_PROP_9_6_8 (specialized) + // dst = regs[obj][regs[key]] + var dst = 9; + var obj = regs[base + 6]; + var key = regs[base + 8]; + regs[base + dst] = obj[key]; + break; + } + case 73: + { + // SET_PROP_3_4_6 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 3]; + var key = regs[base + 4]; + var val = regs[base + 6]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 74: + { + // RETURN_3 (specialized) + var retVal = regs[base + 3]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 75: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 76: + { + // FINALLY_SETUP_83_4_5_94 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 83, + contReg: 4, + payloadReg: 5, + throwPad: 94 + }); + break; + } + case 77: + { + // TRY_SETUP_71_3 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 71, + exceptionReg: 3 + }); + break; + } + case 78: + { + // LT_7_0_6 (specialized) + var dst = 7; + var a = regs[base + 0]; + regs[base + dst] = a < regs[base + 6]; + break; + } + case 79: + { + // JUMP_IF_FALSE_7_59 (specialized) + var src = 7; + var target = 59; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 80: + { + // LOAD_GLOBAL_6_7_0 (specialized) + var dst = 6; + var globalName = this._constant(7, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 81: + { + // THROW_8 (specialized) + throw regs[base + 8]; + break; + } + case 82: + { + // ADD_7_0_6 (specialized) + var dst = 7; + var a = regs[base + 0]; + regs[base + dst] = a + regs[base + 6]; + break; + } + case 83: + { + // MOVE_5_7 (specialized) + var dst = 5; + regs[base + dst] = regs[base + 7]; + break; + } + case 84: + { + // LOAD_INT_4_95 (specialized) + var dst = 4; + regs[base + dst] = 95; + break; + } + case 85: + { + // JUMP_80 (specialized) + regs[fp + SLOTS.PC] = 80; + break; + } + case 86: + { + // GET_PROP_7_3_6 (specialized) + // dst = regs[obj][regs[key]] + var dst = 7; + var obj = regs[base + 3]; + var key = regs[base + 6]; + regs[base + dst] = obj[key]; + break; + } + case 87: + { + // MOVE_5_7 (specialized) + var dst = 5; + regs[base + dst] = regs[base + 7]; + break; + } + case 88: + { + // LOAD_INT_4_96 (specialized) + var dst = 4; + regs[base + dst] = 96; + break; + } + case 89: + { + // LOAD_INT_4_97 (specialized) + var dst = 4; + regs[base + dst] = 97; + break; + } + case 90: + { + // LOAD_GLOBAL_6_0_0 (specialized) + var dst = 6; + var globalName = this._constant(0, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 91: + { + // SET_PROP_6_7_8 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 6]; + var key = regs[base + 7]; + var val = regs[base + 8]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 92: + { + // JUMP_REG_4 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 4]; + break; + } + case 93: + { + // THROW_5 (specialized) + throw regs[base + 5]; + break; + } + case 94: + { + // RETURN_6 (specialized) + var retVal = regs[base + 6]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/qualification.json new file mode 100644 index 00000000..018d3b03 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/qualification.json @@ -0,0 +1,69 @@ +{ + "id": "exceptions--specializedOpcodes--22", + "case": "exceptions", + "profile": "specializedOpcodes", + "seed": 22, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/specialized-runtime-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/encoded.js new file mode 100644 index 00000000..f687349f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/encoded.js @@ -0,0 +1,1436 @@ +var CONSTANTS = [/* 0 */0, /* 1 */42683, /* 2 */"F0Dvl", /* 3 */2667, /* 4 */28697, /* 5 */"Date", /* 6 */"Infinity", /* 7 */17949, /* 8 */"String", /* 9 */"iI5DCis6CY", /* 10 */"Object", /* 11 */62116, /* 12 */"1NROqobZ", /* 13 */"parseInt", /* 14 */"oVJuj", /* 15 */1841, /* 16 */"isFinite", /* 17 */"VQl8", /* 18 */"Math", /* 19 */"3cubbUr", /* 20 */11572, /* 21 */"31B2Hyl", /* 22 */"parseFloat", /* 23 */"6KzD88D", /* 24 */11423, /* 25 */"NaN", /* 26 */"Ke5F", /* 27 */54882, /* 28 */"oq6VOi", /* 29 */"Boolean", /* 30 */56990, /* 31 */"Number", /* 32 */"S9saLFv8", /* 33 */"undefined", /* 34 */32024, /* 35 */"SlTHV0vF7", /* 36 */"Function", /* 37 */"1rK7G", /* 38 */28180, /* 39 */"egDVWoywhS", /* 40 */26667, /* 41 */"cz5iCuwBS", /* 42 */"Error", /* 43 */14789, /* 44 */"isNaN", /* 45 */"40mhG", /* 46 */"XKS", /* 47 */6656, /* 48 */"CcMAkk5bL", /* 49 */"mAHb6P", /* 50 */"qvjVa1", /* 51 */"Array", /* 52 */"window", /* 53 */"qiuDHv6h", /* 54 */"Mcl4WnBxHR", /* 55 */"Sxzvvv", /* 56 */"M1MJSbeJEuB+oct/dFsWyDEdLsmYM5pswtXRlBkkF6zfXbv4CQpK6usPxiAiMdEBqvC7c3iiCP2YO8x3Jll5DW6FaRjNDtX1Fs4pAGn7kBLt+AOH2zGKrS0NKcdd6b7TF3Ln8jYSz7bj+sDWPqRAhnsISdm91kiMAFAP56jta3JpWbkEy/M3UDFwmMwM3mXri24ENW6lyZ+UtiLwhDRiR2VYiYkIB8BJp9TOKufG/WRMay1lANjEeVD9i0LMFUO1RgX3uy3IwaVhSaNTyKaGGxowgLu4+WahMB0MlBHyXQcBlDmEwLe72Nps1IJtEdncAXINOhuV/nXNkV6FrE0TNwzol5kKhSw03SdNQGjSbTBC+ko6GNGLUpFQZL1J6U1fxmcv0s7kZ1bBbfrmOezUiYzV4iS7XwdPniOUT+nmzkIxSHThZR5gNTAP1KybromW/skPX7vboQ6WrMCvZtsPKhONGS/XZa5e+eGilpi/1gQ/rUQXoeT+AiJCsRpK7NoayKbSiAeAywZsXOWCQ+rDJNSpj5qALL4LCX5s1LCIRaO6pM50kB8LdWgpOKqVPwCLiRAbeavhPlmEMAjPh0P/+OlwN9maOZQm7lyyfexuUYJBiKEQLZKfrtJYnHqFm8TH7g1A9HvhMCYAXjpnWARjf8H4wDdrMr4GQgLPjqJ2mn9lxMg6G4atzFXFOxo1noalNRPEewpTwO6gVxQoxubmu2VrALvNQGUS9b5JiT3PHtyBGBHI58H+e0tfR0F9jEGr9U/vNW8dLHYFW1KqOwLCFoRJFuVqzsz40R+LEVciX9dqQW1S5pz4HwyR73O3fjxh8ZLYUvPGC9E/czYTnBjEtWpPlWc1Ow7QBxEJ0pdkZNDxTmTXJj9oMvx5ep8rWiWINNLpdXWTsZ/NJ2rtKRrvlXvo7alRZLy6L+FjZAmpvVb/R4cP26sP4RQhIBj+Nw+5rIHljYMPlHehVsUxvX+Jqf9BFP9/qrbrJKi+vK1iTjZ/KXQvUsnSZxHEW/yS38etpYOI/j1eJTItkMdN4/jAEj20HM4VD9V8", /* 57 */"AWH", /* 58 */undefined, /* 59 */"u6pu", /* 60 */48056, /* 61 */27884, /* 62 */"Jgvcnmh", /* 63 */"qomP", /* 64 */55948, /* 65 */12903, /* 66 */"JSON", /* 67 */"6S49qM", /* 68 */"xFsLe", /* 69 */53142, /* 70 */"CwsC3fNjl", /* 71 */"Jqb6Kxdg", /* 72 */2924, /* 73 */"25rArEZxR2", /* 74 */47375, /* 75 */"YeOCUG", /* 76 */24758, /* 77 */"Tyr", /* 78 */"jgoRMkFYOs", /* 79 */17976, /* 80 */"k4h", /* 81 */45354, /* 82 */"k95cGpurnE", /* 83 */5726, /* 84 */"U2pJ", /* 85 */"ZiYpfdRm", /* 86 */27769, /* 87 */"u8UByfFk", /* 88 */27834, /* 89 */"zDYq2hVpv", /* 90 */"rGL", /* 91 */3970, /* 92 */"whl5rPoYxh", /* 93 */11794, /* 94 */"wCoBgvfdxh", /* 95 */53192, /* 96 */"RegExp", /* 97 */"P3Q7", /* 98 */"aioynFY", /* 99 */40412, /* 100 */"E5n5VZDA", /* 101 */10888, /* 102 */"AOwsC", /* 103 */"fAuB", /* 104 */"length", /* 105 */"Skk", /* 106 */2, /* 107 */"charCodeAt", /* 108 */1, /* 109 */"fromCharCode", /* 110 */"atob", /* 111 */"", /* 112 */8, /* 113 */63033, /* 114 */47340, /* 115 */51832, /* 116 */32204, /* 117 */22951, /* 118 */"0JS", /* 119 */65304, /* 120 */"ufs", /* 121 */18970, /* 122 */"4u52", /* 123 */14299, /* 124 */"2yQ", /* 125 */51863, /* 126 */"fKKY", /* 127 */23558, /* 128 */"MZp6t7Wct", /* 129 */4640, /* 130 */"RJjNWqq8Y", /* 131 */27848, /* 132 */"60FS", /* 133 */"dzL", /* 134 */19429, /* 135 */"lo7QIxt", /* 136 */16088, /* 137 */"cRsLDV8", /* 138 */12433, /* 139 */"R1JAGrE7s", /* 140 */24606, /* 141 */"0g5OYX", /* 142 */"As7Een9qB", /* 143 */8828, /* 144 */"gMjm2xEkB", /* 145 */22996, /* 146 */"zEPXlhPf", /* 147 */16576, /* 148 */"QaLC", /* 149 */"rwJR", /* 150 */8921, /* 151 */"XIioUl1c", /* 152 */10531, /* 153 */"NUwvz7LgdZ", /* 154 */"f7ADh", /* 155 */65535, /* 156 */13, /* 157 */"AJT759U", /* 158 */"zqKFYA2", /* 159 */"JFiwr", /* 160 */"e5c6Zidn", /* 161 */"nq6QpZ", /* 162 */2654435769, /* 163 */33657, /* 164 */"0v5eZNqq", /* 165 */21931, /* 166 */30449, /* 167 */85553907, /* 168 */"imul", /* 169 */238252161, /* 170 */26, /* 171 */"JX5", /* 172 */"Pvel4", /* 173 */2898630115, /* 174 */"kSuVpbZ", /* 175 */"sNO", /* 176 */2607, /* 177 */52380, /* 178 */16, /* 179 */27, /* 180 */"oJ7D327q", /* 181 */1419270675, /* 182 */5, /* 183 */"W4g", /* 184 */"p7R0Rzu0", /* 185 */"voPhDksj", /* 186 */28, /* 187 */14024, /* 188 */"j01", /* 189 */1264810449, /* 190 */2185280040]; +var BYTECODE = [0, 63, 0, 0, 0, 61, 1, 0, 0, 60, 2, 0, 0, 62, 3, 0, 47, 56, 8520, 2, 13, 0, 0, 4, 14, 0, 15, 4, 0, 61, 60, 60, 61, 60, 60, 16, 60, 0, 15, 60, 5, 60, 60, 60, 0, 17, 0, 0, 61, 60, 61, 60, 61, 61, 18, 61, 0, 17, 61, 6, 60, 61, 60, 0, 19, 7, 0, 62, 19, 60, 60, 20, 60, 16, 8, 0, 61, 63, 60, 20, 61, 61, 60, 60, 61, 60, 60, 58, 61, 64, 0, 62, 61, 58, 10, 60, 61, 58, 103, 0, 60, 61, 9, 1, 54, 2110296585, 1, 59, 2276153035, 65, 59, 60, 61, 61, 59, 61, 60, 61, 60, 61, 60, 61, 54, 61, 61, 66, 61, 2, 58, 59, 60, 61, 59, 61, 60, 61, 60, 60, 60, 67, 60, 54, 59, 60, 61, 61, 61, 61, 54, 1, 55, 813913254, 39, 2430, 0, 21, 11, 0, 61, 61, 61, 61, 61, 61, 18, 60, 0, 21, 61, 8, 61, 61, 61, 68, 18, 61, 16, 61, 61, 61, 61, 15545, 22, 60, 60, 63, 60, 22, 61, 60, 61, 60, 61, 61, 60, 58, 61, 64, 0, 62, 61, 58, 13, 60, 61, 58, 211, 0, 60, 61, 12, 69, 61, 61, 58, 58, 60, 60, 61, 61, 1, 54, 2350841921, 1, 59, 486898761, 65, 59, 60, 61, 61, 59, 60, 60, 61, 61, 60, 61, 60, 54, 61, 60, 70, 61, 60, 61, 60, 257, 1, 60, 0, 58, 59, 14, 60, 59, 63, 67, 61, 54, 59, 61, 61, 60, 60, 61, 54, 1, 55, 133258221, 39, 2430, 1, 54, 3745243699, 1, 55, 3256292914, 39, 2430, 0, 23, 15, 0, 65, 24, 60, 60, 60, 18, 60, 60, 60, 61, 60, 61, 60, 23, 60, 60, 61, 60, 61, 61, 61, 60, 18, 61, 0, 24, 60, 16, 61, 61, 60, 68, 18, 61, 16, 61, 60, 61, 61, 11969, 25, 61, 60, 63, 61, 25, 61, 60, 60, 61, 60, 61, 61, 58, 61, 64, 0, 62, 60, 58, 18, 60, 60, 58, 353, 0, 60, 61, 17, 69, 61, 60, 58, 58, 60, 61, 61, 61, 1, 54, 1386395919, 1, 59, 3817420055, 65, 59, 61, 61, 60, 59, 61, 60, 60, 60, 61, 61, 61, 54, 61, 60, 70, 61, 60, 60, 60, 399, 1, 60, 0, 58, 59, 19, 60, 59, 63, 67, 60, 54, 59, 60, 61, 61, 61, 61, 54, 1, 55, 3317471897, 39, 2430, 1, 54, 2342743378, 1, 55, 2848960442, 39, 2430, 0, 26, 20, 0, 65, 27, 60, 61, 60, 18, 60, 60, 60, 61, 60, 61, 61, 26, 60, 60, 61, 61, 61, 61, 61, 61, 18, 60, 0, 27, 60, 8, 61, 60, 61, 68, 18, 60, 16, 61, 61, 60, 60, 57445, 28, 61, 60, 63, 61, 28, 60, 61, 60, 60, 60, 60, 60, 58, 60, 64, 0, 62, 61, 58, 22, 61, 60, 58, 495, 0, 61, 61, 21, 69, 60, 60, 58, 58, 60, 61, 60, 61, 1, 54, 1346317137, 1, 59, 49454953, 65, 59, 61, 60, 61, 59, 61, 60, 61, 60, 60, 60, 60, 54, 61, 61, 70, 61, 61, 61, 61, 541, 2, 60, 0, 59, 58, 23, 61, 59, 63, 67, 61, 54, 59, 60, 60, 61, 60, 60, 54, 1, 55, 2863929418, 39, 2430, 1, 54, 1037268259, 1, 55, 1669926378, 39, 2430, 0, 29, 24, 0, 67, 61, 30, 29, 61, 60, 61, 61, 61, 18, 61, 61, 61, 60, 60, 60, 18, 61, 0, 30, 61, 25, 61, 60, 60, 68, 18, 61, 16, 60, 61, 61, 60, 10468, 31, 60, 61, 63, 61, 31, 61, 61, 60, 61, 60, 60, 60, 58, 60, 64, 0, 62, 61, 58, 22, 61, 61, 58, 631, 0, 61, 60, 26, 1, 54, 2461994339, 1, 59, 30970203, 65, 59, 61, 60, 61, 59, 61, 60, 61, 60, 61, 61, 61, 54, 61, 61, 66, 61, 2, 59, 58, 61, 61, 59, 60, 61, 61, 60, 60, 61, 67, 61, 54, 59, 60, 60, 61, 61, 61, 54, 1, 55, 765842427, 39, 2430, 1, 54, 3737796774, 1, 55, 890170461, 39, 2430, 0, 32, 27, 0, 65, 33, 60, 61, 61, 18, 61, 61, 61, 61, 61, 61, 61, 32, 60, 60, 61, 61, 61, 61, 60, 60, 18, 60, 0, 33, 60, 13, 61, 61, 61, 68, 18, 61, 16, 61, 61, 60, 61, 49933, 34, 61, 61, 63, 60, 34, 61, 60, 61, 60, 60, 61, 60, 58, 61, 64, 0, 62, 61, 58, 29, 60, 60, 58, 763, 0, 60, 60, 28, 1, 54, 1861665542, 1, 59, 3725608702, 65, 59, 60, 60, 60, 59, 60, 60, 60, 61, 60, 60, 61, 54, 61, 60, 66, 60, 2, 59, 58, 60, 60, 59, 60, 60, 60, 60, 60, 60, 67, 61, 54, 59, 61, 60, 61, 60, 60, 54, 1, 55, 1587985236, 39, 2430, 1, 54, 3475215058, 1, 55, 1027638606, 39, 2430, 0, 35, 30, 0, 67, 60, 36, 35, 61, 60, 61, 61, 61, 18, 61, 61, 60, 61, 60, 61, 18, 61, 0, 36, 60, 31, 60, 61, 61, 68, 18, 61, 16, 61, 61, 60, 60, 41052, 37, 60, 61, 63, 61, 37, 61, 61, 60, 60, 60, 60, 60, 58, 60, 64, 0, 62, 60, 58, 33, 61, 61, 58, 889, 0, 60, 60, 32, 1, 54, 1838558786, 1, 59, 3702501946, 65, 59, 61, 60, 61, 59, 61, 61, 61, 61, 61, 61, 60, 54, 60, 61, 66, 61, 0, 58, 59, 61, 61, 59, 61, 61, 60, 61, 61, 60, 67, 60, 54, 59, 60, 61, 60, 61, 60, 54, 1, 55, 139719187, 39, 2430, 1, 54, 3247527099, 1, 55, 3016827378, 39, 2430, 0, 38, 34, 0, 65, 39, 60, 60, 60, 18, 60, 60, 61, 60, 60, 61, 60, 38, 61, 61, 61, 61, 60, 61, 60, 61, 18, 60, 0, 39, 61, 8, 61, 60, 61, 68, 18, 60, 16, 60, 60, 61, 60, 5185, 40, 61, 60, 63, 60, 40, 60, 60, 61, 60, 61, 60, 60, 58, 60, 64, 0, 62, 61, 58, 36, 60, 60, 58, 1021, 0, 61, 61, 35, 69, 61, 60, 58, 58, 60, 60, 60, 60, 1, 54, 4163049223, 1, 59, 30782159, 65, 59, 61, 60, 61, 59, 61, 61, 61, 60, 60, 60, 61, 54, 60, 60, 70, 61, 60, 60, 61, 1067, 0, 61, 0, 58, 59, 37, 60, 59, 63, 67, 60, 54, 59, 60, 61, 61, 60, 61, 54, 1, 55, 3966336247, 39, 2430, 1, 54, 226627668, 1, 55, 3727993424, 39, 2430, 0, 41, 38, 0, 65, 42, 61, 60, 61, 18, 61, 61, 60, 61, 61, 60, 61, 41, 60, 61, 61, 60, 61, 60, 60, 60, 18, 60, 0, 42, 61, 5, 61, 60, 60, 68, 18, 61, 16, 60, 61, 60, 60, 1099, 43, 61, 60, 63, 60, 43, 61, 61, 61, 61, 60, 61, 60, 58, 61, 64, 0, 62, 61, 58, 22, 61, 60, 58, 1163, 0, 61, 60, 39, 1, 54, 3380890249, 1, 59, 949866113, 65, 59, 61, 61, 61, 59, 61, 60, 61, 61, 60, 60, 60, 54, 61, 61, 66, 61, 0, 59, 58, 61, 61, 59, 61, 61, 61, 61, 61, 60, 67, 61, 54, 59, 61, 61, 61, 61, 61, 54, 1, 55, 80945655, 39, 2430, 1, 54, 4229576129, 1, 55, 74932110, 39, 2430, 0, 44, 40, 0, 67, 61, 45, 44, 61, 61, 61, 61, 61, 18, 61, 60, 61, 61, 60, 61, 18, 60, 0, 45, 60, 36, 61, 60, 61, 68, 18, 61, 16, 61, 60, 60, 61, 16470, 46, 60, 61, 63, 61, 46, 60, 60, 61, 60, 61, 60, 61, 58, 60, 64, 0, 62, 60, 58, 42, 61, 61, 58, 1289, 0, 60, 61, 41, 1, 54, 2829500912, 1, 59, 398476776, 65, 59, 60, 61, 60, 59, 60, 61, 61, 60, 60, 61, 60, 54, 60, 60, 66, 61, 2, 58, 59, 61, 61, 59, 61, 61, 60, 60, 61, 60, 67, 61, 54, 59, 60, 60, 61, 60, 61, 54, 1, 55, 888368474, 39, 2430, 1, 54, 1901305541, 1, 55, 1352397613, 39, 2430, 0, 47, 43, 0, 67, 61, 48, 47, 61, 60, 60, 60, 60, 18, 61, 60, 60, 61, 60, 60, 18, 60, 0, 48, 60, 44, 61, 60, 60, 68, 18, 61, 16, 60, 60, 60, 60, 52319, 49, 61, 60, 63, 60, 49, 60, 61, 60, 60, 61, 61, 61, 58, 60, 64, 0, 62, 60, 58, 5, 61, 61, 58, 1415, 0, 61, 61, 45, 69, 61, 61, 58, 58, 61, 61, 60, 61, 1, 54, 1077065569, 1, 59, 3508089705, 65, 59, 61, 60, 61, 59, 60, 61, 60, 60, 61, 61, 60, 54, 60, 61, 70, 60, 61, 60, 60, 1461, 1, 60, 0, 58, 59, 46, 60, 59, 63, 67, 60, 54, 59, 60, 61, 60, 60, 60, 54, 1, 55, 3899101154, 39, 2430, 1, 54, 3186464831, 1, 55, 4272756761, 39, 2430, 0, 50, 47, 0, 67, 60, 51, 50, 61, 61, 61, 61, 61, 18, 61, 61, 61, 61, 61, 60, 18, 60, 0, 51, 61, 25, 61, 61, 61, 68, 18, 60, 16, 60, 60, 60, 61, 33467, 52, 61, 60, 63, 61, 52, 61, 61, 61, 61, 60, 61, 61, 58, 61, 64, 0, 62, 60, 58, 36, 60, 60, 58, 1551, 0, 60, 61, 48, 69, 60, 61, 58, 58, 60, 60, 61, 60, 1, 54, 3268235246, 1, 59, 1971373062, 65, 59, 60, 60, 60, 59, 60, 61, 61, 61, 61, 61, 60, 54, 60, 61, 70, 61, 60, 61, 61, 1597, 1, 60, 0, 58, 59, 49, 61, 59, 63, 67, 61, 54, 59, 61, 61, 60, 60, 60, 54, 1, 55, 2107129722, 39, 2430, 1, 54, 2871724709, 1, 55, 2994230478, 39, 2430, 1, 54, 963735779, 1, 55, 4085905635, 39, 2430, 1, 13, 11, 42, 5, 10, 4, 7, 11, 12, 13, 1, 11, 4016674594, 1, 53, 16640, 67, 60, 16, 53, 60, 60, 60, 60, 60, 16, 1, 54, 3583544717, 1, 55, 2175784617, 39, 2430, 1, 12, 229, 1, 13, 1, 42, 6, 10, 4, 7, 11, 12, 13, 1, 53, 54882, 65, 16, 61, 60, 61, 16, 61, 61, 60, 60, 61, 60, 60, 53, 61, 60, 1, 54, 2050487677, 1, 55, 415446655, 39, 2430, 1, 53, 2130, 71, 53, 60, 61, 61, 61, 61, 61, 53, 0, 16, 63, 60, 53, 60, 61, 60, 60, 61, 60, 60, 58, 61, 64, 0, 62, 60, 58, 51, 61, 61, 58, 1744, 0, 60, 61, 50, 1, 54, 4006473538, 1, 59, 2740413877, 65, 59, 61, 60, 60, 59, 61, 60, 61, 61, 61, 60, 61, 54, 60, 60, 66, 60, 0, 58, 59, 61, 61, 59, 61, 60, 61, 60, 61, 61, 67, 60, 54, 59, 60, 61, 60, 60, 60, 54, 1, 55, 903557224, 39, 2430, 1, 53, 30065, 67, 60, 16, 53, 60, 60, 60, 60, 60, 16, 1, 54, 969170088, 1, 55, 583007002, 39, 2430, 1, 53, 19932, 67, 61, 16, 53, 61, 61, 61, 61, 61, 16, 1, 54, 3677597576, 1, 55, 4071668174, 39, 2430, 2, 4, 52, 0, 1, 11, 96446143, 1, 12, 214, 1, 53, 41456, 67, 61, 16, 53, 61, 60, 60, 60, 60, 16, 1, 54, 3233099784, 1, 55, 3937101384, 39, 2430, 42, 64, 2, 0, 48, 65, 2, 6, 64, 9, 4, 5, 65, 1, 53, 56990, 67, 60, 16, 53, 60, 60, 60, 60, 60, 16, 1, 54, 3890348679, 1, 55, 766248056, 39, 2430, 47, 10, 5268, 4, 87, 0, 0, 4, 1, 47, 3, 2446, 0, 9, 1, 0, 1, 7, 1, 53, 1439, 65, 16, 61, 61, 61, 16, 61, 61, 60, 60, 61, 60, 61, 53, 61, 60, 1, 54, 21632280, 1, 55, 3675835316, 39, 2430, 1, 53, 55718, 71, 53, 61, 61, 61, 60, 61, 61, 53, 1, 16, 63, 61, 53, 61, 61, 61, 61, 60, 60, 60, 58, 61, 64, 0, 62, 60, 58, 31, 60, 60, 58, 1987, 0, 60, 60, 53, 1, 54, 3751724132, 1, 59, 4186907399, 65, 59, 60, 60, 60, 59, 60, 60, 61, 60, 61, 61, 61, 54, 60, 61, 66, 60, 1, 59, 58, 61, 61, 59, 60, 61, 61, 61, 61, 60, 67, 61, 54, 59, 61, 61, 60, 61, 60, 54, 1, 55, 228290315, 39, 2430, 1, 53, 10133, 65, 16, 60, 60, 61, 16, 60, 61, 61, 61, 60, 61, 60, 53, 60, 60, 1, 54, 3275058262, 1, 55, 1178396113, 39, 2430, 1, 53, 10133, 65, 16, 61, 60, 61, 16, 61, 60, 60, 60, 60, 61, 61, 53, 60, 60, 1, 54, 4101203100, 1, 55, 2365700389, 39, 2430, 1, 53, 4934, 71, 53, 61, 60, 60, 60, 61, 61, 53, 1, 16, 63, 61, 53, 61, 61, 61, 60, 60, 61, 60, 58, 60, 64, 0, 62, 60, 58, 25, 60, 61, 58, 2132, 0, 60, 60, 54, 69, 61, 60, 58, 58, 61, 60, 60, 61, 1, 54, 3066241719, 1, 59, 2327772894, 65, 59, 60, 60, 61, 59, 61, 60, 61, 60, 60, 61, 60, 54, 60, 61, 70, 61, 60, 61, 61, 2178, 1, 60, 0, 58, 59, 55, 60, 59, 63, 67, 60, 54, 59, 60, 61, 60, 61, 61, 54, 1, 55, 3837416503, 39, 2430, 1, 53, 33419, 65, 16, 60, 61, 60, 16, 61, 60, 60, 61, 60, 61, 61, 53, 60, 61, 1, 54, 228941105, 1, 55, 4037489681, 39, 2430, 1, 53, 13413, 65, 16, 61, 60, 60, 16, 60, 60, 60, 60, 60, 60, 60, 53, 60, 60, 1, 54, 3751195854, 1, 55, 1801383506, 39, 2430, 47, 8, 2483, 1, 77, 0, 0, 0, 9, 56, 0, 42, 7, 8, 1, 9, 1, 53, 21445, 67, 60, 16, 53, 60, 60, 60, 60, 60, 16, 1, 54, 3534435443, 1, 55, 476277864, 39, 2430, 61, 61, 61, 60, 60, 60, 2, 61, 0, 3, 60, 8, 61, 61, 60, 63, 60, 2, 61, 60, 60, 60, 61, 61, 60, 58, 61, 64, 0, 62, 61, 58, 16, 61, 61, 58, 2325, 0, 60, 60, 57, 1, 54, 2072227872, 1, 59, 14781957, 65, 59, 60, 60, 60, 59, 60, 61, 61, 60, 61, 60, 61, 54, 61, 61, 66, 61, 1, 59, 58, 60, 61, 59, 60, 61, 60, 61, 60, 61, 67, 61, 54, 59, 61, 61, 60, 60, 60, 54, 1, 55, 965370870, 39, 2430, 1, 53, 13531, 67, 61, 16, 53, 61, 60, 60, 60, 60, 16, 1, 54, 716000733, 1, 55, 2084028102, 39, 2430, 1, 53, 46673, 65, 16, 61, 61, 61, 16, 61, 60, 60, 61, 61, 60, 60, 53, 61, 61, 1, 54, 2399466167, 1, 55, 3496742039, 39, 2430, 0, 64, 58, 0, 45, 64, 42, 54, 56, 2, 55, 54, 0, 57, 59, 0, 8, 54, 54, 57, 58, 54, 3, 3, 0, 47, 4, 5268, 4, 87, 0, 0, 4, 1, 1, 5, 2909690708, 1, 6, 232, 1, 7, 1, 42, 2, 4, 4, 3, 5, 6, 7, 45, 2, 0, 8, 58, 0, 45, 8, 0, 74, 0, 0, 0, 73, 60, 0, 0, 70, 61, 0, 0, 71, 62, 0, 0, 72, 63, 0, 47, 67, 8927, 3, 15, 0, 0, 4, 15, 0, 16, 64, 0, 61, 70, 70, 71, 70, 70, 17, 70, 0, 16, 70, 25, 70, 70, 70, 0, 18, 0, 0, 61, 70, 71, 72, 70, 70, 19, 71, 0, 18, 70, 8, 71, 70, 72, 0, 20, 65, 0, 62, 20, 72, 70, 21, 71, 17, 66, 0, 71, 63, 72, 21, 70, 70, 71, 72, 70, 72, 72, 68, 72, 64, 0, 73, 70, 68, 5, 70, 70, 68, 2590, 0, 71, 72, 67, 69, 70, 71, 68, 68, 72, 70, 70, 70, 1, 64, 2074556791, 1, 69, 2017309446, 65, 69, 70, 70, 70, 69, 72, 72, 71, 71, 70, 71, 72, 64, 70, 72, 70, 71, 70, 72, 72, 2636, 0, 71, 0, 69, 68, 68, 72, 69, 74, 67, 71, 64, 69, 72, 70, 72, 70, 70, 64, 1, 65, 16927, 1, 66, 44110, 39, 5259, 0, 22, 69, 0, 61, 72, 71, 72, 70, 70, 19, 71, 0, 22, 71, 5, 72, 71, 70, 68, 19, 71, 17, 71, 70, 71, 71, 44782, 23, 71, 70, 63, 70, 23, 70, 72, 70, 72, 70, 71, 71, 68, 70, 64, 0, 73, 72, 68, 42, 72, 72, 68, 2711, 0, 70, 70, 70, 69, 70, 70, 68, 68, 70, 70, 71, 70, 1, 64, 3874148358, 1, 69, 3301134311, 65, 69, 70, 71, 71, 69, 70, 72, 71, 70, 70, 70, 71, 64, 70, 70, 70, 70, 71, 70, 71, 2757, 0, 71, 0, 68, 69, 71, 72, 69, 74, 67, 70, 64, 69, 72, 72, 71, 71, 71, 64, 1, 65, 55597, 1, 66, 38959, 39, 5259, 1, 64, 630433535, 1, 65, 47014, 1, 66, 43508, 39, 5259, 0, 24, 72, 0, 67, 71, 25, 24, 72, 71, 72, 72, 72, 19, 61, 70, 72, 72, 70, 71, 19, 70, 0, 25, 72, 33, 72, 72, 71, 68, 19, 70, 17, 70, 70, 72, 70, 46329, 26, 72, 72, 63, 72, 26, 70, 70, 72, 72, 72, 72, 70, 68, 70, 64, 0, 73, 72, 68, 18, 70, 70, 68, 2853, 0, 70, 70, 73, 1, 64, 1268118893, 1, 69, 695104844, 65, 69, 70, 72, 72, 69, 70, 70, 71, 70, 70, 72, 70, 64, 72, 72, 66, 70, 1, 69, 68, 72, 70, 69, 71, 70, 72, 70, 71, 71, 67, 72, 64, 69, 70, 71, 70, 71, 71, 64, 1, 65, 65251, 1, 66, 50552, 39, 5259, 1, 64, 1795622184, 1, 65, 38109, 1, 66, 35844, 39, 5259, 0, 27, 74, 0, 65, 28, 71, 70, 71, 19, 71, 70, 70, 72, 72, 70, 70, 27, 71, 71, 61, 71, 72, 70, 72, 70, 19, 71, 0, 28, 71, 8, 70, 71, 72, 68, 19, 71, 17, 71, 70, 72, 72, 21874, 29, 71, 71, 63, 70, 29, 71, 71, 72, 70, 72, 71, 72, 68, 71, 64, 0, 73, 71, 68, 66, 71, 71, 68, 2991, 0, 70, 72, 75, 1, 64, 800896020, 1, 69, 3887903757, 65, 69, 72, 70, 72, 69, 71, 72, 71, 72, 71, 70, 71, 64, 70, 70, 66, 70, 2, 69, 68, 70, 71, 69, 72, 71, 71, 70, 72, 71, 67, 72, 64, 69, 70, 70, 70, 70, 70, 64, 1, 65, 15895, 1, 66, 23653, 39, 5259, 1, 64, 2005959222, 1, 65, 37657, 1, 66, 24640, 39, 5259, 0, 30, 76, 0, 67, 72, 31, 30, 72, 72, 72, 72, 72, 19, 61, 71, 72, 70, 72, 72, 19, 72, 0, 31, 72, 66, 71, 72, 72, 68, 19, 71, 17, 72, 72, 70, 72, 18986, 32, 70, 70, 63, 70, 32, 72, 70, 72, 72, 72, 70, 70, 68, 70, 64, 0, 73, 71, 68, 6, 71, 72, 68, 3123, 0, 72, 71, 77, 69, 72, 71, 68, 68, 72, 72, 70, 71, 1, 64, 2755187299, 1, 69, 1547227772, 65, 69, 71, 72, 72, 69, 72, 70, 72, 70, 72, 72, 72, 64, 72, 72, 70, 72, 71, 71, 72, 3169, 2, 72, 0, 68, 69, 78, 71, 69, 74, 67, 71, 64, 69, 71, 72, 70, 70, 72, 64, 1, 65, 23785, 1, 66, 2431, 39, 5259, 1, 64, 2924514112, 1, 65, 6979, 1, 66, 62365, 39, 5259, 0, 33, 79, 0, 67, 70, 34, 33, 70, 70, 72, 70, 70, 19, 61, 70, 71, 71, 71, 70, 19, 70, 0, 34, 70, 25, 71, 71, 70, 68, 19, 71, 17, 71, 70, 70, 71, 11169, 35, 71, 70, 63, 71, 35, 70, 72, 71, 70, 71, 70, 72, 68, 70, 64, 0, 73, 71, 68, 51, 72, 70, 68, 3265, 0, 70, 70, 80, 1, 64, 1693082378, 1, 69, 1120068323, 65, 69, 71, 72, 70, 69, 71, 70, 70, 70, 71, 70, 70, 64, 71, 71, 66, 70, 0, 69, 68, 70, 70, 69, 71, 70, 72, 70, 70, 72, 67, 70, 64, 69, 71, 71, 71, 72, 72, 64, 1, 65, 60900, 1, 66, 37582, 39, 5259, 1, 64, 3364773241, 1, 65, 27901, 1, 66, 18276, 39, 5259, 0, 36, 81, 0, 65, 37, 72, 71, 71, 19, 71, 71, 70, 71, 71, 70, 70, 36, 70, 71, 61, 71, 71, 72, 71, 70, 19, 72, 0, 37, 70, 29, 71, 70, 71, 68, 19, 71, 17, 71, 70, 72, 71, 52798, 38, 72, 71, 63, 71, 38, 70, 71, 71, 71, 71, 71, 70, 68, 71, 64, 0, 73, 72, 68, 66, 71, 71, 68, 3403, 0, 72, 72, 82, 1, 64, 2942641077, 1, 69, 1734681516, 65, 69, 71, 71, 71, 69, 72, 72, 71, 71, 72, 71, 70, 64, 72, 71, 66, 71, 0, 69, 68, 70, 70, 69, 70, 72, 71, 71, 72, 72, 67, 72, 64, 69, 72, 72, 71, 71, 72, 64, 1, 65, 12037, 1, 66, 36879, 39, 5259, 1, 64, 3528949897, 1, 65, 4009, 1, 66, 1640, 39, 5259, 0, 39, 83, 0, 65, 40, 70, 72, 70, 19, 72, 72, 70, 71, 72, 70, 71, 39, 70, 70, 61, 70, 71, 72, 70, 70, 19, 72, 0, 40, 70, 5, 72, 70, 72, 68, 19, 71, 17, 70, 72, 71, 72, 15814, 41, 72, 71, 63, 71, 41, 72, 72, 71, 72, 71, 71, 71, 68, 71, 64, 0, 73, 72, 68, 33, 72, 71, 68, 3541, 0, 72, 72, 84, 69, 72, 72, 68, 68, 72, 72, 72, 71, 1, 64, 728473410, 1, 69, 155459373, 65, 69, 72, 72, 72, 69, 72, 71, 71, 72, 72, 71, 71, 64, 70, 71, 70, 71, 72, 71, 72, 3587, 1, 72, 0, 69, 68, 85, 71, 69, 74, 67, 71, 64, 69, 70, 71, 71, 72, 72, 64, 1, 65, 43800, 1, 66, 21683, 39, 5259, 1, 64, 219077926, 1, 65, 60990, 1, 66, 51725, 39, 5259, 0, 42, 86, 0, 67, 70, 43, 42, 70, 71, 71, 71, 71, 19, 61, 70, 72, 70, 70, 70, 19, 71, 0, 43, 71, 18, 72, 71, 71, 68, 19, 72, 17, 72, 70, 70, 70, 10815, 44, 72, 71, 63, 70, 44, 72, 72, 71, 70, 71, 71, 71, 68, 72, 64, 0, 73, 72, 68, 5, 72, 70, 68, 3683, 0, 71, 71, 87, 1, 64, 3980410697, 1, 69, 3407396640, 65, 69, 72, 71, 71, 69, 71, 72, 72, 72, 71, 72, 71, 64, 71, 71, 66, 71, 1, 69, 68, 71, 72, 69, 70, 72, 72, 72, 71, 70, 67, 70, 64, 69, 72, 70, 71, 70, 72, 64, 1, 65, 19345, 1, 66, 57126, 39, 5259, 1, 64, 3635415310, 1, 65, 6484, 1, 66, 38003, 39, 5259, 0, 45, 88, 0, 67, 71, 46, 45, 72, 71, 72, 72, 72, 19, 61, 70, 71, 72, 72, 72, 19, 70, 0, 46, 72, 18, 72, 70, 71, 68, 19, 72, 17, 70, 71, 70, 71, 58286, 47, 70, 72, 63, 72, 47, 70, 70, 72, 72, 72, 72, 72, 68, 70, 64, 0, 73, 72, 68, 6, 72, 70, 68, 3815, 0, 72, 72, 89, 69, 72, 72, 68, 68, 72, 72, 72, 70, 1, 64, 2628679190, 1, 69, 1420719631, 65, 69, 72, 70, 71, 69, 72, 72, 70, 72, 72, 72, 72, 64, 71, 72, 70, 72, 72, 71, 72, 3861, 1, 71, 0, 69, 68, 90, 70, 69, 74, 67, 70, 64, 69, 70, 71, 71, 70, 71, 64, 1, 65, 713, 1, 66, 41306, 39, 5259, 1, 64, 2625007126, 1, 65, 65173, 1, 66, 40623, 39, 5259, 0, 48, 91, 0, 65, 49, 71, 70, 71, 19, 71, 72, 70, 70, 70, 70, 70, 48, 72, 71, 61, 70, 71, 71, 70, 70, 19, 70, 0, 49, 70, 44, 70, 70, 70, 68, 19, 70, 17, 71, 72, 72, 71, 22129, 50, 70, 70, 63, 70, 50, 72, 71, 72, 71, 71, 70, 71, 68, 70, 64, 0, 73, 72, 68, 36, 71, 72, 68, 3963, 0, 70, 71, 92, 1, 64, 912545023, 1, 69, 3999552740, 65, 69, 71, 72, 71, 69, 72, 72, 70, 72, 70, 72, 70, 64, 70, 72, 66, 71, 2, 68, 69, 70, 70, 69, 71, 71, 71, 71, 72, 72, 67, 71, 64, 69, 71, 70, 71, 70, 72, 64, 1, 65, 57897, 1, 66, 5037, 39, 5259, 1, 64, 3394849759, 1, 65, 23719, 1, 66, 777, 39, 5259, 0, 51, 93, 0, 67, 71, 52, 51, 71, 71, 71, 71, 72, 19, 61, 71, 71, 71, 72, 71, 19, 70, 0, 52, 71, 29, 70, 70, 72, 68, 19, 71, 17, 71, 70, 70, 70, 34326, 53, 72, 70, 63, 70, 53, 72, 70, 72, 70, 70, 71, 70, 68, 71, 64, 0, 73, 71, 68, 22, 70, 72, 68, 4095, 0, 71, 72, 94, 1, 64, 1364051313, 1, 69, 791037285, 65, 69, 70, 70, 70, 69, 72, 70, 70, 72, 72, 72, 72, 64, 70, 72, 66, 72, 1, 69, 68, 71, 70, 69, 71, 70, 71, 71, 72, 70, 67, 72, 64, 69, 70, 72, 72, 71, 70, 64, 1, 65, 21271, 1, 66, 63285, 39, 5259, 1, 64, 4169084412, 1, 65, 15852, 1, 66, 10227, 39, 5259, 0, 54, 95, 0, 65, 55, 72, 70, 72, 19, 72, 72, 71, 72, 71, 70, 71, 54, 72, 72, 61, 70, 70, 70, 71, 72, 19, 72, 0, 55, 71, 96, 71, 72, 72, 68, 19, 71, 17, 70, 70, 71, 71, 36257, 56, 72, 72, 63, 72, 56, 72, 71, 70, 71, 71, 72, 70, 68, 70, 64, 0, 73, 71, 68, 8, 71, 70, 68, 4233, 0, 71, 70, 97, 69, 71, 70, 68, 68, 70, 71, 71, 71, 1, 64, 4144048828, 1, 69, 2936089271, 65, 69, 71, 70, 70, 69, 72, 72, 71, 71, 72, 70, 71, 64, 71, 72, 70, 70, 72, 70, 70, 4279, 1, 70, 0, 69, 68, 98, 70, 69, 74, 67, 70, 64, 69, 71, 71, 72, 71, 71, 64, 1, 65, 14533, 1, 66, 49070, 39, 5259, 1, 64, 126588493, 1, 65, 42690, 1, 66, 53149, 39, 5259, 0, 57, 99, 0, 67, 72, 58, 57, 72, 70, 72, 72, 72, 19, 61, 72, 72, 71, 71, 72, 19, 70, 0, 58, 72, 66, 72, 72, 71, 68, 19, 72, 17, 70, 70, 72, 72, 15333, 59, 71, 71, 63, 71, 59, 70, 70, 72, 72, 72, 70, 70, 68, 70, 64, 0, 73, 70, 68, 29, 70, 70, 68, 4375, 0, 71, 70, 100, 1, 64, 4185067883, 1, 69, 2977108322, 65, 69, 70, 72, 70, 69, 70, 71, 70, 70, 72, 70, 70, 64, 70, 70, 66, 70, 2, 69, 68, 70, 71, 69, 70, 70, 70, 70, 70, 71, 67, 72, 64, 69, 72, 71, 72, 72, 70, 64, 1, 65, 8195, 1, 66, 35518, 39, 5259, 1, 64, 1100555471, 1, 65, 13790, 1, 66, 16024, 39, 5259, 0, 60, 101, 0, 67, 72, 61, 60, 72, 72, 72, 72, 72, 19, 61, 72, 72, 72, 72, 72, 19, 71, 0, 61, 71, 16, 71, 71, 72, 68, 19, 72, 17, 72, 72, 70, 72, 5833, 62, 72, 71, 63, 71, 62, 70, 71, 70, 72, 71, 70, 72, 68, 71, 64, 0, 73, 72, 68, 33, 71, 71, 68, 4507, 0, 72, 72, 102, 69, 70, 72, 68, 68, 72, 71, 71, 72, 1, 64, 3927900053, 1, 69, 3354886005, 65, 69, 71, 70, 72, 69, 70, 70, 72, 72, 71, 71, 72, 64, 70, 70, 70, 70, 70, 72, 70, 4553, 1, 72, 0, 69, 68, 103, 71, 69, 74, 67, 70, 64, 69, 70, 70, 72, 72, 72, 64, 1, 65, 3715, 1, 66, 46088, 39, 5259, 1, 64, 1705699772, 1, 65, 64091, 1, 66, 55819, 39, 5259, 1, 64, 2386401301, 1, 65, 8282, 1, 66, 28782, 39, 5259, 0, 75, 104, 0, 8, 76, 3, 75, 71, 75, 72, 71, 71, 72, 70, 72, 76, 2, 5, 63, 71, 75, 71, 72, 70, 72, 70, 71, 71, 68, 71, 64, 0, 73, 70, 68, 10, 71, 70, 68, 4638, 0, 72, 71, 105, 1, 64, 552520603, 1, 69, 991316925, 65, 69, 70, 71, 71, 69, 70, 71, 72, 72, 70, 71, 72, 64, 70, 71, 66, 71, 0, 68, 69, 70, 70, 69, 71, 72, 71, 70, 71, 72, 67, 71, 64, 69, 71, 71, 72, 70, 70, 64, 1, 65, 23329, 1, 66, 34205, 39, 5259, 1, 63, 45907, 67, 72, 17, 63, 72, 72, 72, 72, 71, 17, 1, 64, 190368189, 1, 65, 21010, 1, 66, 60809, 39, 5259, 1, 63, 4509, 67, 71, 17, 63, 71, 71, 71, 71, 71, 17, 1, 64, 675932210, 1, 65, 28813, 1, 66, 2682, 39, 5259, 0, 75, 106, 0, 67, 72, 76, 75, 71, 70, 71, 71, 71, 5, 61, 70, 71, 70, 71, 71, 5, 70, 0, 76, 72, 25, 70, 71, 72, 1, 63, 45354, 65, 17, 71, 71, 71, 17, 71, 71, 71, 70, 70, 70, 70, 63, 71, 71, 1, 64, 208324040, 1, 65, 19999, 1, 66, 61083, 39, 5259, 0, 75, 107, 0, 8, 11, 3, 75, 0, 13, 108, 0, 1, 63, 47375, 65, 17, 70, 71, 70, 17, 72, 72, 71, 72, 71, 72, 71, 63, 71, 71, 1, 64, 2193125492, 1, 65, 63674, 1, 66, 25832, 39, 5259, 0, 75, 107, 0, 8, 76, 3, 75, 43, 10, 3, 76, 1, 5, 1, 63, 22617, 67, 70, 17, 63, 70, 71, 71, 71, 71, 17, 1, 64, 1866225912, 1, 65, 57675, 1, 66, 20844, 39, 5259, 72, 12, 2, 70, 70, 70, 70, 70, 14, 70, 71, 70, 70, 70, 70, 75, 73, 75, 72, 70, 10, 70, 70, 70, 72, 70, 70, 76, 70, 43, 9, 7, 8, 1, 76, 1, 63, 12780, 65, 17, 72, 72, 70, 17, 71, 70, 70, 70, 70, 72, 70, 63, 71, 71, 1, 64, 254792338, 1, 65, 23273, 1, 66, 61792, 39, 5259, 0, 75, 0, 0, 61, 70, 71, 72, 70, 71, 5, 72, 0, 75, 72, 25, 72, 72, 72, 1, 63, 22043, 65, 17, 72, 71, 72, 17, 71, 72, 71, 70, 70, 71, 71, 63, 72, 71, 1, 64, 475142948, 1, 65, 41853, 1, 66, 65154, 39, 5259, 2, 7, 8, 0, 0, 75, 109, 0, 8, 8, 7, 75, 1, 63, 22869, 67, 70, 17, 63, 71, 71, 71, 71, 71, 17, 1, 64, 1757939930, 1, 65, 37165, 1, 66, 19192, 39, 5259, 4, 2, 2, 75, 110, 0, 42, 6, 75, 1, 0, 1, 63, 2806, 65, 17, 71, 72, 72, 17, 71, 71, 70, 71, 70, 72, 70, 63, 71, 72, 1, 64, 309449754, 1, 65, 23662, 1, 66, 62626, 39, 5259, 0, 75, 58, 0, 45, 75, 61, 71, 72, 72, 71, 71, 3, 72, 0, 6, 72, 18, 72, 71, 71, 0, 75, 111, 0, 61, 71, 71, 71, 71, 71, 4, 70, 0, 75, 72, 22, 72, 72, 71, 1, 63, 25028, 65, 17, 72, 72, 71, 17, 70, 70, 71, 72, 70, 71, 70, 63, 70, 70, 1, 64, 95397920, 1, 65, 12406, 1, 66, 59360, 39, 5259, 45, 4, 67, 72, 75, 13, 72, 72, 70, 72, 72, 5, 43, 12, 3, 11, 1, 75, 0, 14, 112, 0, 1, 63, 55081, 67, 72, 17, 63, 70, 72, 72, 72, 72, 17, 1, 64, 3661424403, 1, 65, 28510, 1, 66, 48237, 39, 5259, 1, 63, 50647, 67, 72, 17, 63, 70, 72, 70, 70, 72, 17, 1, 64, 2420522473, 1, 65, 50769, 1, 66, 29302, 39, 5259, 67, 70, 75, 9, 72, 72, 71, 71, 71, 4, 61, 71, 70, 70, 70, 70, 4, 71, 0, 75, 70, 33, 71, 71, 70, 1, 63, 433, 67, 72, 17, 63, 71, 70, 71, 72, 71, 17, 1, 64, 14759986, 1, 65, 49288, 1, 66, 58129, 39, 5259, 42, 64, 67, 3, 64, 65, 66, 58, 64, 0, 84, 0, 0, 0, 81, 113, 0, 0, 82, 114, 0, 0, 83, 115, 0, 47, 78, 9325, 3, 13, 0, 0, 4, 20, 0, 21, 116, 0, 61, 82, 82, 82, 81, 81, 22, 81, 0, 21, 81, 51, 82, 81, 81, 0, 23, 0, 0, 61, 81, 82, 82, 82, 81, 24, 81, 0, 23, 82, 31, 81, 81, 82, 0, 25, 117, 0, 62, 25, 81, 81, 26, 82, 22, 33, 0, 81, 63, 81, 26, 82, 82, 81, 81, 81, 82, 81, 79, 82, 64, 0, 83, 82, 79, 42, 82, 82, 79, 5371, 0, 81, 81, 118, 1, 75, 2235076651, 1, 80, 3703902053, 65, 80, 82, 82, 82, 80, 81, 82, 81, 82, 82, 81, 82, 75, 82, 81, 66, 81, 1, 80, 79, 82, 82, 80, 82, 81, 81, 81, 82, 81, 67, 81, 75, 80, 82, 82, 81, 81, 82, 75, 1, 76, 29594, 1, 77, 2912, 39, 8511, 0, 27, 119, 0, 61, 82, 82, 82, 82, 82, 24, 82, 0, 27, 82, 44, 82, 82, 81, 68, 24, 82, 22, 82, 82, 81, 81, 54645, 28, 81, 82, 63, 82, 28, 82, 82, 81, 81, 82, 81, 82, 79, 82, 64, 0, 83, 81, 79, 6, 81, 82, 79, 5482, 0, 82, 82, 120, 1, 75, 3920559533, 1, 80, 1366062413, 65, 80, 82, 81, 81, 80, 81, 81, 81, 82, 82, 82, 82, 75, 81, 82, 66, 82, 0, 80, 79, 81, 82, 80, 82, 81, 82, 81, 81, 82, 67, 82, 75, 80, 82, 82, 82, 82, 81, 75, 1, 76, 62555, 1, 77, 26727, 39, 8511, 1, 75, 683936498, 1, 76, 9137, 1, 77, 55247, 39, 8511, 0, 29, 121, 0, 65, 30, 81, 82, 81, 24, 81, 81, 81, 82, 81, 82, 81, 29, 82, 82, 61, 82, 82, 81, 82, 81, 24, 82, 0, 30, 81, 44, 82, 81, 81, 68, 24, 82, 22, 82, 82, 81, 81, 18257, 31, 82, 81, 63, 82, 31, 82, 81, 81, 82, 81, 81, 81, 79, 82, 64, 0, 83, 82, 79, 8, 81, 82, 79, 5620, 0, 81, 81, 122, 1, 75, 728197658, 1, 80, 1671618817, 65, 80, 81, 81, 82, 80, 82, 81, 82, 81, 82, 81, 82, 75, 82, 81, 66, 81, 2, 80, 79, 82, 81, 80, 82, 81, 81, 82, 81, 82, 67, 82, 75, 80, 81, 82, 82, 81, 82, 75, 1, 76, 61562, 1, 77, 62100, 39, 8511, 1, 75, 2404951883, 1, 76, 63314, 1, 77, 31227, 39, 8511, 0, 32, 123, 0, 67, 81, 33, 32, 81, 82, 82, 82, 82, 24, 61, 81, 82, 81, 82, 82, 24, 81, 0, 33, 81, 36, 82, 82, 81, 68, 24, 82, 22, 82, 82, 81, 82, 38210, 34, 81, 81, 63, 81, 34, 81, 81, 81, 82, 81, 81, 82, 79, 81, 64, 0, 83, 81, 79, 22, 82, 81, 79, 5752, 0, 82, 81, 124, 1, 75, 3366195901, 1, 80, 2447809503, 65, 80, 82, 81, 82, 80, 81, 82, 81, 81, 81, 81, 81, 75, 81, 81, 66, 82, 1, 80, 79, 81, 82, 80, 82, 81, 81, 82, 81, 81, 67, 81, 75, 80, 81, 81, 81, 82, 82, 75, 1, 76, 15639, 1, 77, 47541, 39, 8511, 1, 75, 1375593770, 1, 76, 37062, 1, 77, 31684, 39, 8511, 0, 35, 125, 0, 65, 36, 81, 82, 82, 24, 81, 82, 82, 82, 81, 81, 81, 35, 81, 82, 61, 81, 81, 81, 82, 81, 24, 81, 0, 36, 81, 36, 81, 81, 81, 68, 24, 81, 22, 82, 81, 82, 81, 36315, 37, 81, 81, 63, 82, 37, 81, 82, 82, 82, 82, 81, 82, 79, 81, 64, 0, 83, 82, 79, 31, 81, 82, 79, 5890, 0, 82, 82, 126, 1, 75, 3523434724, 1, 80, 2605101019, 65, 80, 81, 81, 81, 80, 81, 82, 82, 81, 82, 81, 82, 75, 82, 82, 66, 81, 2, 80, 79, 81, 82, 80, 81, 81, 81, 82, 81, 81, 67, 81, 75, 80, 82, 81, 81, 81, 82, 75, 1, 76, 39161, 1, 77, 6968, 39, 8511, 1, 75, 1594340027, 1, 76, 36960, 1, 77, 43241, 39, 8511, 0, 38, 127, 0, 67, 81, 39, 38, 81, 81, 81, 81, 82, 24, 61, 81, 81, 81, 81, 81, 24, 81, 0, 39, 82, 51, 81, 81, 81, 68, 24, 81, 22, 81, 82, 81, 81, 45485, 40, 81, 82, 63, 81, 40, 82, 81, 82, 81, 81, 82, 82, 79, 82, 64, 0, 83, 81, 79, 33, 82, 82, 79, 6022, 0, 82, 82, 128, 1, 75, 3274492127, 1, 80, 3035517411, 65, 80, 82, 82, 81, 80, 81, 82, 82, 82, 81, 82, 82, 75, 81, 81, 66, 81, 0, 80, 79, 82, 81, 80, 81, 82, 81, 82, 81, 82, 67, 81, 75, 80, 81, 82, 82, 81, 82, 75, 1, 76, 40327, 1, 77, 41845, 39, 8511, 1, 75, 810008698, 1, 76, 17085, 1, 77, 60125, 39, 8511, 0, 41, 129, 0, 67, 81, 42, 41, 81, 82, 82, 82, 82, 24, 61, 82, 81, 82, 81, 82, 24, 81, 0, 42, 82, 96, 82, 81, 81, 68, 24, 82, 22, 82, 81, 81, 81, 13450, 43, 82, 82, 63, 82, 43, 82, 82, 82, 81, 82, 82, 82, 79, 82, 64, 0, 83, 82, 79, 8, 81, 82, 79, 6154, 0, 82, 82, 130, 1, 75, 2254780701, 1, 80, 3752505907, 65, 80, 81, 82, 82, 80, 82, 82, 82, 81, 82, 82, 81, 75, 82, 82, 66, 82, 1, 79, 80, 81, 82, 80, 82, 82, 82, 82, 82, 81, 67, 82, 75, 80, 82, 81, 82, 81, 82, 75, 1, 76, 54157, 1, 77, 28947, 39, 8511, 1, 75, 2400841988, 1, 76, 25788, 1, 77, 64380, 39, 8511, 0, 44, 131, 0, 65, 45, 81, 81, 81, 24, 81, 81, 81, 81, 81, 81, 81, 44, 81, 81, 61, 82, 81, 82, 82, 82, 24, 81, 0, 45, 81, 13, 82, 82, 81, 68, 24, 81, 22, 81, 81, 82, 82, 2848, 46, 82, 81, 63, 81, 46, 81, 81, 81, 81, 82, 82, 81, 79, 82, 64, 0, 83, 81, 79, 36, 82, 81, 79, 6292, 0, 81, 82, 132, 69, 82, 81, 79, 79, 82, 81, 82, 81, 1, 75, 1975376275, 1, 80, 3137290612, 65, 80, 82, 81, 82, 80, 82, 82, 81, 82, 82, 82, 82, 75, 82, 82, 70, 81, 81, 82, 82, 6338, 2, 82, 0, 80, 79, 133, 81, 80, 84, 67, 82, 75, 80, 82, 81, 82, 81, 82, 75, 1, 76, 43896, 1, 77, 18865, 39, 8511, 1, 75, 3679452816, 1, 76, 33391, 1, 77, 3306, 39, 8511, 0, 47, 134, 0, 67, 82, 48, 47, 81, 81, 81, 81, 81, 24, 61, 82, 82, 81, 82, 82, 24, 82, 0, 48, 82, 13, 82, 82, 81, 68, 24, 82, 22, 82, 82, 81, 82, 9735, 49, 81, 82, 63, 81, 49, 81, 81, 81, 82, 81, 81, 81, 79, 82, 64, 0, 83, 81, 79, 5, 82, 81, 79, 6434, 0, 81, 81, 135, 1, 75, 1379463674, 1, 80, 192841348, 65, 80, 81, 81, 81, 80, 82, 82, 82, 82, 81, 82, 81, 75, 82, 81, 66, 82, 0, 80, 79, 81, 81, 80, 81, 81, 81, 81, 81, 81, 67, 82, 75, 80, 81, 82, 82, 81, 82, 75, 1, 76, 63951, 1, 77, 33042, 39, 8511, 1, 75, 4216181474, 1, 76, 8469, 1, 77, 6029, 39, 8511, 0, 50, 136, 0, 67, 81, 51, 50, 82, 82, 82, 82, 82, 24, 61, 82, 82, 81, 82, 82, 24, 81, 0, 51, 82, 5, 82, 82, 82, 68, 24, 81, 22, 82, 81, 81, 82, 2607, 52, 82, 82, 63, 82, 52, 81, 82, 82, 81, 82, 81, 81, 79, 81, 64, 0, 83, 82, 79, 66, 82, 81, 79, 6566, 0, 81, 81, 137, 1, 75, 2081256135, 1, 80, 935394104, 65, 80, 81, 82, 81, 80, 81, 81, 81, 82, 82, 82, 81, 75, 81, 81, 66, 82, 2, 80, 79, 81, 82, 80, 81, 82, 82, 82, 81, 82, 67, 82, 75, 80, 82, 82, 81, 82, 81, 75, 1, 76, 61364, 1, 77, 5706, 39, 8511, 1, 75, 779458240, 1, 76, 45899, 1, 77, 58909, 39, 8511, 0, 53, 138, 0, 65, 54, 82, 81, 82, 24, 82, 81, 81, 81, 81, 81, 81, 53, 82, 82, 61, 81, 82, 82, 81, 81, 24, 82, 0, 54, 81, 66, 82, 81, 82, 68, 24, 81, 22, 81, 81, 82, 81, 4873, 55, 82, 81, 63, 82, 55, 81, 81, 82, 82, 81, 81, 82, 79, 82, 64, 0, 83, 82, 79, 51, 82, 81, 79, 6704, 0, 82, 82, 139, 1, 75, 517961776, 1, 80, 2769940764, 65, 80, 82, 81, 81, 80, 82, 82, 82, 82, 82, 81, 82, 75, 82, 81, 66, 81, 2, 80, 79, 81, 82, 80, 81, 81, 81, 81, 82, 82, 67, 82, 75, 80, 82, 81, 81, 82, 82, 75, 1, 76, 21999, 1, 77, 56493, 39, 8511, 1, 75, 1981054892, 1, 76, 26087, 1, 77, 62540, 39, 8511, 0, 56, 140, 0, 67, 82, 57, 56, 81, 81, 81, 81, 81, 24, 61, 81, 82, 81, 82, 81, 24, 81, 0, 57, 82, 5, 81, 81, 82, 68, 24, 81, 22, 81, 81, 82, 82, 36432, 58, 81, 82, 63, 81, 58, 82, 82, 82, 82, 81, 82, 82, 79, 82, 64, 0, 83, 81, 79, 22, 82, 81, 79, 6836, 0, 82, 81, 141, 69, 82, 82, 79, 79, 81, 82, 82, 82, 1, 75, 2404351371, 1, 80, 931722411, 65, 80, 82, 82, 81, 80, 81, 81, 82, 82, 81, 82, 82, 75, 81, 82, 70, 81, 81, 81, 81, 6882, 1, 82, 0, 79, 80, 142, 82, 80, 84, 67, 82, 75, 80, 81, 82, 81, 82, 81, 75, 1, 76, 50831, 1, 77, 51893, 39, 8511, 1, 75, 660271674, 1, 76, 41080, 1, 77, 16966, 39, 8511, 0, 59, 143, 0, 65, 60, 82, 81, 82, 24, 82, 82, 82, 82, 82, 81, 82, 59, 82, 82, 61, 81, 82, 81, 81, 81, 24, 82, 0, 60, 81, 33, 82, 82, 82, 68, 24, 82, 22, 82, 81, 81, 81, 28879, 61, 81, 82, 63, 82, 61, 82, 81, 81, 81, 81, 82, 82, 79, 82, 64, 0, 83, 82, 79, 33, 82, 81, 79, 6984, 0, 81, 81, 144, 1, 75, 3428144200, 1, 80, 119004579, 65, 80, 82, 82, 82, 80, 82, 81, 82, 82, 82, 82, 82, 75, 81, 82, 66, 82, 0, 80, 79, 81, 82, 80, 82, 82, 81, 81, 81, 81, 67, 82, 75, 80, 81, 82, 82, 82, 81, 75, 1, 76, 7986, 1, 77, 21104, 39, 8511, 1, 75, 2518635540, 1, 76, 33913, 1, 77, 24533, 39, 8511, 0, 62, 145, 0, 65, 63, 82, 81, 82, 24, 82, 81, 81, 81, 82, 81, 81, 62, 81, 81, 61, 82, 82, 82, 82, 81, 24, 81, 0, 63, 81, 51, 82, 81, 82, 68, 24, 81, 22, 82, 82, 81, 81, 16508, 64, 82, 81, 63, 82, 64, 81, 81, 81, 81, 81, 82, 82, 79, 82, 64, 0, 83, 81, 79, 51, 81, 81, 79, 7122, 0, 82, 81, 146, 1, 75, 2201965264, 1, 80, 3638746062, 65, 80, 82, 81, 82, 80, 81, 81, 81, 82, 81, 82, 82, 75, 81, 82, 66, 82, 2, 80, 79, 81, 81, 80, 82, 81, 82, 81, 82, 82, 67, 82, 75, 80, 81, 82, 82, 82, 82, 75, 1, 76, 35128, 1, 77, 30064, 39, 8511, 1, 75, 4236935041, 1, 76, 17006, 1, 77, 22980, 39, 8511, 0, 65, 147, 0, 67, 81, 66, 65, 81, 82, 82, 82, 82, 24, 61, 82, 82, 82, 82, 82, 24, 82, 0, 66, 82, 33, 82, 82, 82, 68, 24, 82, 22, 81, 82, 82, 81, 46456, 67, 82, 81, 63, 82, 67, 81, 82, 81, 82, 82, 82, 81, 79, 81, 64, 0, 83, 81, 79, 44, 82, 82, 79, 7254, 0, 81, 81, 148, 69, 82, 81, 79, 79, 82, 81, 81, 81, 1, 75, 868297614, 1, 80, 2063835821, 65, 80, 82, 81, 81, 80, 81, 81, 81, 81, 81, 82, 81, 75, 81, 81, 70, 81, 82, 81, 81, 7300, 2, 81, 0, 79, 80, 149, 81, 80, 84, 67, 81, 75, 80, 82, 81, 81, 81, 81, 75, 1, 76, 17027, 1, 77, 18969, 39, 8511, 1, 75, 3020417500, 1, 76, 11055, 1, 77, 41286, 39, 8511, 0, 68, 150, 0, 67, 81, 69, 68, 82, 82, 82, 82, 82, 24, 61, 81, 81, 82, 82, 81, 24, 81, 0, 69, 82, 51, 81, 81, 82, 68, 24, 81, 22, 82, 81, 82, 82, 2496, 70, 81, 82, 63, 82, 70, 81, 82, 81, 82, 82, 81, 81, 79, 82, 64, 0, 83, 82, 79, 33, 82, 82, 79, 7396, 0, 81, 81, 151, 1, 75, 2820810390, 1, 80, 1894755748, 65, 80, 82, 81, 81, 80, 81, 81, 82, 82, 82, 82, 82, 75, 82, 81, 66, 82, 0, 80, 79, 82, 82, 80, 82, 81, 82, 82, 81, 81, 67, 82, 75, 80, 81, 81, 81, 81, 82, 75, 1, 76, 55502, 1, 77, 47319, 39, 8511, 1, 75, 852968625, 1, 76, 39387, 1, 77, 36014, 39, 8511, 0, 71, 152, 0, 65, 72, 82, 82, 82, 24, 82, 81, 82, 82, 82, 82, 82, 71, 82, 82, 61, 82, 82, 82, 82, 81, 24, 82, 0, 72, 82, 6, 82, 81, 82, 68, 24, 81, 22, 82, 82, 82, 82, 162, 73, 82, 81, 63, 81, 73, 81, 82, 82, 82, 81, 82, 82, 79, 82, 64, 0, 83, 82, 79, 13, 81, 82, 79, 7534, 0, 82, 81, 153, 1, 75, 4077149701, 1, 80, 3125269866, 65, 80, 81, 81, 82, 80, 82, 81, 82, 81, 82, 81, 81, 75, 82, 82, 66, 81, 1, 80, 79, 82, 81, 80, 81, 82, 82, 82, 82, 82, 67, 82, 75, 80, 81, 82, 82, 81, 81, 75, 1, 76, 53430, 1, 77, 39460, 39, 8511, 1, 75, 1445983002, 1, 76, 325, 1, 77, 54336, 39, 8511, 1, 75, 3005459079, 1, 76, 34103, 1, 77, 40987, 39, 8511, 61, 82, 81, 82, 81, 82, 6, 82, 0, 18, 82, 36, 82, 81, 81, 1, 74, 38862, 67, 81, 22, 74, 82, 81, 82, 82, 82, 22, 1, 75, 1215420438, 1, 76, 63401, 1, 77, 23369, 39, 8511, 61, 82, 82, 82, 81, 81, 7, 82, 0, 19, 81, 5, 82, 82, 81, 1, 74, 6327, 67, 82, 22, 74, 81, 81, 81, 81, 81, 22, 1, 75, 2070455923, 1, 76, 35279, 1, 77, 26704, 39, 8511, 8, 85, 0, 16, 67, 82, 86, 7, 82, 81, 81, 81, 81, 2, 43, 17, 0, 85, 1, 86, 1, 74, 7342, 67, 82, 22, 74, 82, 82, 82, 82, 82, 22, 1, 75, 2355998496, 1, 76, 32915, 1, 77, 40789, 39, 8511, 4, 5, 0, 85, 111, 0, 61, 81, 81, 81, 82, 82, 6, 81, 0, 85, 81, 16, 81, 82, 82, 1, 74, 12433, 67, 81, 22, 74, 81, 81, 81, 81, 81, 22, 1, 75, 3917115329, 1, 76, 16500, 1, 77, 64066, 39, 8511, 74, 82, 82, 1, 81, 85, 81, 81, 82, 82, 82, 82, 81, 11, 82, 75, 82, 81, 82, 12, 81, 81, 154, 81, 0, 85, 82, 1, 81, 0, 13, 155, 0, 1, 74, 51863, 65, 22, 81, 81, 81, 22, 82, 82, 81, 81, 81, 81, 82, 74, 81, 81, 1, 75, 1705070716, 1, 76, 32705, 1, 77, 30361, 39, 8511, 73, 10, 81, 81, 9, 81, 81, 81, 81, 81, 81, 85, 82, 61, 81, 82, 81, 81, 81, 1, 81, 0, 85, 81, 22, 81, 81, 81, 0, 11, 156, 0, 1, 74, 19071, 67, 81, 22, 74, 81, 81, 81, 82, 82, 22, 1, 75, 1743379578, 1, 76, 62407, 1, 77, 29905, 39, 8511, 1, 74, 61223, 71, 74, 82, 82, 82, 82, 82, 82, 74, 2, 22, 63, 82, 74, 82, 82, 82, 81, 82, 81, 82, 79, 82, 64, 0, 83, 82, 79, 96, 82, 81, 79, 7936, 0, 81, 82, 157, 1, 75, 2005624307, 1, 80, 1807161882, 65, 80, 81, 82, 82, 80, 82, 81, 82, 82, 82, 82, 81, 75, 82, 81, 66, 82, 0, 80, 79, 81, 81, 80, 81, 81, 82, 81, 82, 82, 67, 81, 75, 80, 82, 82, 81, 81, 81, 75, 1, 76, 21426, 1, 77, 33841, 39, 8511, 1, 74, 28336, 67, 82, 22, 74, 82, 82, 81, 82, 82, 22, 1, 75, 3585715441, 1, 76, 33607, 1, 77, 50817, 39, 8511, 1, 74, 23665, 67, 81, 22, 74, 81, 81, 81, 81, 81, 22, 1, 75, 3699031549, 1, 76, 37451, 1, 77, 53058, 39, 8511, 70, 82, 81, 82, 81, 8053, 2, 81, 0, 12, 13, 158, 82, 85, 84, 61, 81, 81, 81, 81, 82, 8, 81, 0, 85, 82, 66, 82, 82, 81, 2, 14, 8, 0, 1, 74, 31182, 67, 81, 22, 74, 81, 82, 81, 82, 81, 22, 1, 75, 4023425207, 1, 76, 46850, 1, 77, 64744, 39, 8511, 45, 6, 0, 85, 109, 0, 8, 15, 14, 85, 0, 16, 107, 0, 1, 74, 14966, 65, 22, 82, 81, 82, 22, 82, 82, 81, 81, 81, 82, 81, 74, 81, 82, 1, 75, 3051472393, 1, 76, 59833, 1, 77, 42713, 39, 8511, 61, 81, 82, 82, 81, 81, 85, 82, 0, 7, 81, 29, 82, 82, 81, 0, 85, 108, 0, 67, 82, 19, 85, 81, 82, 81, 81, 82, 7, 1, 74, 2501, 67, 82, 22, 74, 82, 82, 81, 82, 81, 22, 1, 75, 1092387411, 1, 76, 44524, 1, 77, 21028, 39, 8511, 0, 85, 0, 0, 61, 82, 82, 81, 82, 82, 7, 82, 0, 85, 81, 16, 81, 82, 82, 1, 74, 12173, 67, 82, 22, 74, 82, 81, 82, 82, 81, 22, 1, 75, 2786232437, 1, 76, 42948, 1, 77, 46378, 39, 8511, 75, 82, 82, 82, 85, 82, 82, 159, 82, 0, 8, 82, 17, 81, 43, 86, 14, 15, 1, 85, 67, 81, 18, 86, 81, 82, 81, 82, 82, 6, 1, 74, 23208, 65, 22, 82, 81, 81, 22, 82, 81, 82, 82, 81, 81, 82, 74, 82, 81, 1, 75, 2474913505, 1, 76, 339, 1, 77, 32956, 39, 8511, 71, 85, 81, 82, 82, 82, 81, 81, 3, 0, 7, 63, 82, 85, 82, 81, 82, 82, 82, 82, 82, 79, 82, 64, 0, 83, 82, 79, 51, 81, 82, 79, 8333, 0, 82, 82, 160, 69, 82, 82, 79, 79, 81, 81, 81, 82, 1, 75, 2101166549, 1, 80, 545171734, 65, 80, 81, 81, 81, 80, 81, 82, 82, 82, 82, 82, 82, 75, 82, 81, 70, 81, 81, 82, 81, 8379, 0, 82, 0, 79, 80, 161, 81, 80, 84, 67, 82, 75, 80, 82, 82, 82, 82, 82, 75, 1, 76, 10625, 1, 77, 19551, 39, 8511, 1, 74, 28261, 65, 22, 82, 82, 81, 22, 81, 81, 82, 82, 81, 82, 82, 74, 82, 82, 1, 75, 1496592378, 1, 76, 68, 1, 77, 18956, 39, 8511, 1, 74, 10476, 65, 22, 81, 81, 81, 22, 81, 82, 82, 82, 81, 82, 82, 74, 82, 82, 1, 75, 2974829504, 1, 76, 25712, 1, 77, 41576, 39, 8511, 0, 85, 162, 0, 67, 81, 9, 85, 82, 81, 81, 81, 81, 1, 0, 10, 0, 0, 1, 74, 4772, 65, 22, 81, 82, 81, 22, 82, 82, 82, 81, 81, 81, 81, 74, 81, 82, 1, 75, 3542438493, 1, 76, 32235, 1, 77, 49181, 39, 8511, 0, 85, 58, 0, 45, 85, 42, 75, 78, 3, 75, 76, 77, 58, 75, 0, 8, 0, 0, 0, 6, 163, 0, 0, 4, 164, 0, 0, 5, 165, 0, 0, 7, 166, 0, 4, 3, 0, 9, 167, 0, 67, 5, 10, 9, 4, 5, 4, 4, 4, 1, 0, 9, 0, 0, 74, 6, 6, 10, 4, 11, 6, 5, 6, 4, 5, 4, 6, 9, 4, 61, 5, 6, 6, 4, 5, 1, 6, 0, 11, 4, 6, 5, 5, 6, 2, 9, 18, 0, 0, 10, 168, 0, 8, 11, 9, 10, 0, 10, 169, 0, 43, 12, 9, 11, 2, 1, 10, 0, 9, 0, 0, 74, 6, 6, 12, 6, 10, 5, 6, 6, 5, 5, 6, 6, 9, 5, 61, 5, 5, 6, 5, 6, 1, 4, 0, 10, 6, 8, 5, 6, 6, 0, 9, 170, 0, 74, 4, 4, 1, 6, 10, 4, 5, 6, 5, 5, 6, 5, 9, 6, 75, 6, 6, 4, 9, 5, 5, 171, 5, 0, 10, 5, 1, 5, 0, 10, 0, 0, 74, 6, 5, 9, 4, 11, 4, 5, 6, 5, 5, 5, 6, 10, 6, 61, 4, 4, 5, 5, 5, 1, 4, 0, 11, 5, 8, 5, 6, 6, 75, 4, 4, 5, 9, 5, 6, 172, 5, 0, 0, 4, 1, 5, 0, 10, 0, 0, 74, 5, 5, 9, 5, 11, 6, 4, 4, 5, 5, 4, 5, 10, 6, 61, 4, 6, 6, 6, 4, 1, 4, 0, 11, 6, 16, 6, 4, 4, 76, 8, 6, 5, 26562, 4, 9, 5, 4, 5, 6, 1, 8775, 0, 10, 0, 0, 74, 6, 4, 9, 4, 11, 6, 5, 4, 4, 4, 6, 4, 10, 6, 61, 6, 4, 5, 4, 4, 1, 4, 0, 11, 4, 96, 6, 4, 5, 76, 8, 6, 5, 33245, 5, 9, 4, 6, 5, 6, 1, 8822, 0, 10, 0, 0, 74, 5, 5, 9, 5, 11, 6, 4, 6, 5, 4, 6, 6, 10, 4, 61, 5, 6, 4, 5, 5, 1, 6, 0, 11, 5, 44, 4, 4, 4, 0, 9, 173, 0, 65, 10, 6, 5, 4, 1, 5, 4, 4, 4, 4, 6, 6, 9, 5, 5, 0, 9, 0, 0, 74, 4, 4, 10, 5, 11, 5, 4, 4, 5, 4, 4, 5, 9, 4, 61, 5, 5, 6, 5, 5, 1, 4, 0, 11, 4, 13, 5, 4, 4, 0, 9, 59, 0, 49, 10, 1, 9, 1, 45, 10, 0, 9, 58, 0, 45, 9, 0, 8, 174, 0, 0, 10, 0, 0, 0, 6, 175, 0, 0, 7, 176, 0, 0, 9, 177, 0, 4, 4, 0, 11, 178, 0, 72, 2, 1, 7, 8, 7, 7, 7, 11, 8, 7, 7, 7, 8, 7, 12, 73, 12, 7, 8, 1, 7, 6, 6, 7, 7, 6, 11, 8, 0, 12, 0, 0, 74, 6, 6, 11, 7, 13, 8, 6, 7, 6, 7, 6, 6, 12, 8, 61, 8, 8, 6, 8, 8, 5, 6, 0, 13, 6, 13, 7, 6, 8, 0, 11, 179, 0, 74, 6, 8, 0, 6, 12, 7, 6, 8, 6, 6, 8, 7, 11, 7, 75, 6, 7, 8, 11, 6, 8, 180, 8, 0, 12, 6, 0, 7, 0, 12, 0, 0, 74, 7, 8, 11, 6, 13, 6, 8, 6, 6, 6, 8, 7, 12, 7, 61, 8, 6, 6, 7, 8, 0, 8, 0, 13, 6, 5, 7, 7, 8, 76, 10, 6, 8, 28512, 7, 11, 6, 7, 8, 6, 0, 9096, 0, 12, 0, 0, 74, 8, 8, 11, 8, 13, 8, 6, 6, 6, 6, 8, 8, 12, 6, 61, 8, 8, 7, 8, 7, 0, 7, 0, 13, 8, 29, 8, 7, 6, 67, 8, 11, 5, 6, 6, 8, 6, 8, 0, 0, 12, 0, 0, 74, 6, 6, 11, 7, 13, 6, 6, 8, 6, 8, 6, 8, 12, 6, 61, 8, 8, 6, 6, 6, 0, 6, 0, 13, 6, 25, 7, 7, 8, 2, 11, 18, 0, 0, 12, 168, 0, 8, 13, 11, 12, 0, 12, 181, 0, 43, 14, 11, 13, 2, 0, 12, 0, 11, 0, 0, 74, 7, 7, 14, 7, 12, 6, 7, 6, 6, 6, 7, 7, 11, 7, 61, 6, 7, 7, 6, 6, 0, 8, 0, 12, 6, 13, 7, 6, 6, 0, 11, 182, 0, 72, 0, 0, 6, 7, 6, 8, 7, 11, 6, 8, 8, 6, 8, 6, 12, 0, 11, 179, 0, 74, 8, 6, 0, 7, 13, 8, 7, 8, 6, 6, 6, 8, 11, 6, 73, 13, 8, 8, 12, 6, 7, 8, 6, 6, 7, 11, 8, 0, 12, 0, 0, 74, 7, 7, 11, 7, 13, 8, 7, 6, 7, 7, 7, 8, 12, 8, 61, 7, 7, 7, 7, 7, 0, 8, 0, 13, 8, 25, 6, 6, 8, 45, 0, 0, 11, 58, 0, 45, 11, 0, 7, 183, 0, 0, 6, 184, 0, 0, 8, 185, 0, 4, 4, 0, 9, 178, 0, 72, 2, 2, 8, 8, 8, 8, 6, 9, 6, 8, 6, 7, 7, 7, 10, 73, 10, 6, 8, 1, 8, 8, 8, 7, 7, 7, 9, 8, 0, 10, 0, 0, 74, 8, 7, 9, 7, 11, 8, 8, 6, 6, 7, 8, 8, 10, 7, 61, 7, 8, 7, 6, 7, 5, 8, 0, 11, 6, 51, 6, 8, 8, 0, 9, 186, 0, 74, 7, 6, 0, 7, 10, 8, 8, 8, 6, 7, 6, 8, 9, 6, 75, 6, 7, 7, 9, 7, 6, 187, 6, 0, 10, 7, 0, 6, 0, 10, 0, 0, 74, 7, 8, 9, 8, 11, 8, 7, 6, 8, 7, 7, 7, 10, 7, 61, 6, 7, 7, 7, 7, 0, 6, 0, 11, 6, 51, 8, 6, 6, 75, 8, 7, 6, 9, 6, 7, 188, 7, 0, 5, 6, 0, 8, 0, 10, 0, 0, 74, 8, 8, 9, 8, 11, 6, 7, 7, 6, 6, 8, 8, 10, 7, 61, 6, 6, 8, 6, 8, 0, 8, 0, 11, 8, 13, 7, 8, 6, 2, 9, 18, 0, 0, 10, 168, 0, 8, 11, 9, 10, 0, 10, 189, 0, 43, 12, 9, 11, 2, 0, 10, 0, 9, 0, 0, 74, 8, 6, 12, 8, 10, 7, 6, 8, 7, 7, 6, 7, 9, 7, 61, 6, 6, 6, 6, 7, 0, 6, 0, 10, 7, 6, 6, 7, 7, 0, 9, 190, 0, 67, 6, 10, 9, 8, 6, 7, 6, 7, 0, 0, 9, 0, 0, 74, 6, 6, 10, 6, 11, 6, 7, 6, 6, 6, 6, 7, 9, 6, 61, 8, 7, 7, 8, 7, 0, 8, 0, 11, 6, 8, 7, 7, 6, 45, 0, 0, 9, 58, 0, 45, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 66; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_11_10_9_0_1_12_3_8_13_5_7_6_4_2 (order: [11,10,9,0,1,12,3,8,13,5,7,6,4,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[13], _unsortedOperands[6], _unsortedOperands[12], _unsortedOperands[9], _unsortedOperands[11], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[8]]; + // SHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >> regs[base + _operands[2]]; + // DIV + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a / regs[base + _operands[5]]; + // LOAD_GLOBAL + var dst = _operands[6]; + var globalName = this._constant(_operands[7], _operands[8]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // SUB + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a - regs[base + _operands[11]]; + // MOVE + var dst = _operands[12]; + regs[base + dst] = regs[base + _operands[13]]; + break; + } + case 62: + { + // ANTI_NEQ_8_0_5_6_3_7_1_2_4 (order: [8,0,5,6,3,7,1,2,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[0]]; + // LOAD_GLOBAL + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // BXOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a ^ regs[base + _operands[5]]; + // NEQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a !== regs[base + _operands[8]]; + break; + } + case 63: + { + // ANTI_UNARY_NOT_1_7_3_9_10_2_8_5_0_6_4 (order: [1,7,3,9,10,2,8,5,0,6,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[9], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[4]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[2]]); + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // UNARY_NOT + var dst = _operands[6]; + regs[base + dst] = !regs[base + _operands[7]]; + // MOD + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a % regs[base + _operands[10]]; + break; + } + case 64: + { + // ANTI_UNARY_POS_2_6_5_11_9_3_4_12_7_10_8_0_1 (order: [2,6,5,11,9,3,4,12,7,10,8,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[10], _unsortedOperands[4], _unsortedOperands[9], _unsortedOperands[3], _unsortedOperands[7]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this._constant(_operands[1], _operands[2]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // LOOSE_EQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a == regs[base + _operands[5]]; + // JUMP_IF_FALSE + var src = _operands[6]; + var target = _operands[7]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // LOAD_GLOBAL + var dst = _operands[8]; + var globalName = this._constant(_operands[9], _operands[10]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // UNARY_POS + var dst = _operands[11]; + regs[base + dst] = +regs[base + _operands[12]]; + break; + } + case 65: + { + // ANTI_SUB_12_2_11_1_13_0_4_3_8_5_6_7_14_9_10 (order: [12,2,11,1,13,0,4,3,8,5,6,7,14,9,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[11], _unsortedOperands[8], _unsortedOperands[13], _unsortedOperands[14], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[12]]; + // MUL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a * regs[base + _operands[2]]; + // GTE + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >= regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + // LOOSE_NEQ + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a != regs[base + _operands[11]]; + // SUB + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a - regs[base + _operands[14]]; + break; + } + case 66: + { + // ANTI_MUL_8_7_4_5_12_11_3_6_2_1_0_9_10 (order: [8,7,4,5,12,11,3,6,2,1,0,9,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[5], _unsortedOperands[4]]; + // LOOSE_EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a == regs[base + _operands[2]]; + // MUL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a * regs[base + _operands[5]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[6]; + var pairCount = _operands[7]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[8]]; + var val = regs[base + _operands[9]]; + o[key] = val; + } + regs[base + dst] = o; + // MUL + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a * regs[base + _operands[12]]; + break; + } + case 67: + { + // ANTI_ADD_6_0_2_7_8_4_3_5_1 (order: [6,0,2,7,8,4,3,5,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[4]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // LOOSE_NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a != regs[base + _operands[5]]; + // SUB + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a - regs[base + _operands[8]]; + break; + } + case 68: + { + // ANTI_EQ_7_9_6_0_2_8_10_4_5_1_3 (order: [7,9,6,0,2,8,10,4,5,1,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[6]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // LOAD_INT + var dst = _operands[3]; + regs[base + dst] = _operands[4]; + // EQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a === regs[base + _operands[7]]; + // SHL + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a << regs[base + _operands[10]]; + break; + } + case 69: + { + // ANTI_UNARY_NEG_6_7_4_3_5_2_0_1 (order: [6,7,4,3,5,2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[1]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // UNARY_NEG + var dst = _operands[3]; + regs[base + dst] = -regs[base + _operands[4]]; + // LTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a <= regs[base + _operands[7]]; + break; + } + case 70: + { + // ANTI_BAND_9_2_8_10_4_1_0_7_12_13_6_5_11_3 (order: [9,2,8,10,4,1,0,7,12,13,6,5,11,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[13], _unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[10], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[12], _unsortedOperands[8], _unsortedOperands[9]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[0]; + var count = _operands[1]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[2]]; + regs[base + dst] = elems; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[3]; + var target = _operands[4]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[5]; + var name = this._constant(_operands[6], _operands[7]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // LT + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a < regs[base + _operands[10]]; + // BAND + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a & regs[base + _operands[13]]; + break; + } + case 71: + { + // ANTI_LT_0_3_6_8_5_9_4_2_7_1 (order: [0,3,6,8,5,9,4,2,7,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[5]]; + // LT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; + // BXOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a ^ regs[base + _operands[5]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[6]; + var pairCount = _operands[7]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[8]]; + var val = regs[base + _operands[9]]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + case 72: + { + // ANTI_SHL_4_7_11_1_2_0_9_5_6_13_14_10_8_12_3 (order: [4,7,11,1,2,0,9,5,6,13,14,10,8,12,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[14], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[12], _unsortedOperands[6], _unsortedOperands[11], _unsortedOperands[2], _unsortedOperands[13], _unsortedOperands[9], _unsortedOperands[10]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[6]; + var pairCount = _operands[7]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[8]]; + var val = regs[base + _operands[9]]; + o[key] = val; + } + regs[base + dst] = o; + // GTE + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a >= regs[base + _operands[12]]; + // UNARY_NOT + var dst = _operands[13]; + regs[base + dst] = !regs[base + _operands[14]]; + break; + } + case 73: + { + // ANTI_BOR_2_9_4_1_6_8_5_7_10_3_0_11 (order: [2,9,4,1,6,8,5,7,10,3,0,11]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[11]]; + // BOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a | regs[base + _operands[2]]; + // UNARY_NEG + var dst = _operands[3]; + regs[base + dst] = -regs[base + _operands[4]]; + // UNARY_NOT + var dst = _operands[5]; + regs[base + dst] = !regs[base + _operands[6]]; + // UNARY_BITNOT + var dst = _operands[7]; + regs[base + dst] = ~regs[base + _operands[8]]; + // LT + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a < regs[base + _operands[11]]; + break; + } + case 74: + { + // ANTI_USHR_3_10_1_7_0_6_9_4_8_12_11_5_2_13 (order: [3,10,1,7,0,6,9,4,8,12,11,5,2,13]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[12], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[10], _unsortedOperands[9], _unsortedOperands[13]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // SHL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a << regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + // ADD + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a + regs[base + _operands[11]]; + // UNARY_NOT + var dst = _operands[12]; + regs[base + dst] = !regs[base + _operands[13]]; + break; + } + case 75: + { + // ANTI_BXOR_5_1_0_2_9_11_6_12_7_4_10_3_8 (order: [5,1,0,2,9,11,6,12,7,4,10,3,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[11], _unsortedOperands[9], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[12], _unsortedOperands[4], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[7]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // BXOR + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a ^ regs[base + _operands[4]]; + // LOAD_CONST + var dst = _operands[5]; + regs[base + dst] = this._constant(_operands[6], _operands[7]); + // EXP + // Math.pow instead of `**` + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[10]]); + // MOVE + var dst = _operands[11]; + regs[base + dst] = regs[base + _operands[12]]; + break; + } + case 76: + { + // ANTI_UNARY_BITNOT_4_7_10_3_11_0_8_6_9_2_1_5 (order: [4,7,10,3,11,0,8,6,9,2,1,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[9], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[4]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // LOAD_INT + var dst = _operands[2]; + regs[base + dst] = _operands[3]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[4]; + var target = _operands[5]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // LOOSE_NEQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a != regs[base + _operands[8]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[11]]); + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/qualification.json new file mode 100644 index 00000000..a78a073c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/qualification.json @@ -0,0 +1,71 @@ +{ + "id": "mainBranchSharedStrings--stringConcealingControlFlowFlatteningDispatcherAntiInstrumentation--2", + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "case": "mainBranchSharedStrings", + "profile": "stringConcealingControlFlowFlatteningDispatcherAntiInstrumentation", + "seed": 2, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "a", + "b" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "a", + "b" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "a", + "b" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/source.js b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/source.js new file mode 100644 index 00000000..92569828 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-anti-instrumentation-main-branch/source.js @@ -0,0 +1 @@ +function f() { return "b"; } if (f) window.TEST_OUTPUT = ["a", f()]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/encoded.js new file mode 100644 index 00000000..e8d439a4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/encoded.js @@ -0,0 +1,1392 @@ +var CONSTANTS = [/* 0 */-442192490, /* 1 */1367702323, /* 2 */"FunXYSiprjYcWw3PG3sf0ZApFaG8JiSTg/8zg+wGCLHxKevm3HqY7yhbmMY+yYVRGumpYuCfwjOLtoUCwomj8aea0S6uu9RW7MraQc7J8VLSPvGj7Wfx8gMOlIEFOqiODRrpZ9zqyl7HyWPC7F4P8rV3LeLsriUy7pkuDnP6RxZXqoY+GqqC0VB+hcIVRluyey5oYlnmA24hGieGSApPnlNqOuZ2XimyQDYEQkXOaXJz1lsiDLqGFiqKjv8VimoWA7poImTmH3KjzyRClMYBsqlddObsagaenQvzhsAavY0d9p5iJy6rsyBW3MLpfZ/VyariPq+r/BfH+rAO8IW4MvauoePUZ//ygF78xcvFil4A689nIxu2rho174XNDp/ygxc3o+w+d1WK1XB9uco5V7a7dS9bZUn1BY2yAgyH0DNOn5BiEuVzXWbNb8cIW1fPV3sr5WAtRL1gFyODKP9Rk2MVVL1LLVHVXnvbzyhbqTRdrapdKuV+c2afeQP4hBAdtW0Y5aJkFy/9uwlHqMzsfdnV/aQhP5+TIheB/AMNqpWoNM6s59vmd8/M7Vz+xd411Vwv49Nn7BzyjMsF+4XaDMj8wWepq6s8EFSixUd8mcwWVJWj2Cz8lK31dIz9HC20hCsYnK5kKvSLXWDMSsR3VGrsqmQj5NAtVrzbBHWEU/x+lBAkdLx+LEzUSkRzzEVUWzRirDQcfvRUdHqcGQQrtBccnW0K5JVUQMy9u3BEfMx9fHHk46RqPLiTIQT1/AQMuZU1JPSs59uAd/nMH1y/tSw0i1wY7Nx3rxz9jPsFiZXLbNT8qmf+q+Q8tUXAxeF8xOzGJ9ij+yyZlNUFI42wHDG3rSsO37N0yfX9XYDMBbexUxHvmWQxlfANeL2BBBOLav9da1klmr1rTdzELkOLzxpTkiRurRsdSPUFC1mfegtMt0Idb2029VNTaM8+w01Xcu1GfSDlfZUvP5+LEwe5+gBtraVmJUWvBNMpR7HCHl3xtRkll54A65F3BBvmjsY1nZX5bmPjoldU05k+BEXV1f5+4urjJ9K7wQ7NmtUFwY3QErenyyP/3sly1cXaXcTK67bzWyPu1XoslqENPYKYFoKThP/DYg/Wvb0qSrLGNXqu7x9asCYvqlMSBIZfAq+fLTLcpi8iv20o9kxacM51OlpWUep9fnwWO5IwPkWCOhZg8ipuQ6Z2OltOeto3RqfKSF6sqTAi354Z8luGYQJ+jo0xEpqQbi36sVcnyqu+LkqV0Tt+weLVF/2zoA4Dgqn5E26vGlKm/yv/3sZKgMHaWakyrabmI8bv7HL9mfoB8LqKFtprx/+davXWxLkNQYPWC3OC7wKj4zbQsd0Z+IYJC4SfGSuhpnDZmnE0+ZdCVM95MxunBeGLeQgRtooFP4+LIRcu+VVhWNl4MSZPW8NTd3jzR1l/oWMpKp9eC1+HfhtcsWE5XKGgaAjz20cCw+q4JFFX6QRhc+vzFzq78ABzmbXxfWn3IBq3nzOG2P9An8kMUeMoGqeVGwePrHjegdgJ3rDWKN5ju//OULbIqkHnSOrQxXv879SY8zCCicAB9IhOcIafFCCjqFjQ7kllwMdY96/VO/6gLuifcXcIlYAAOJiTC2j74AloetBEOGFAidtVcJDoCiCpqWwwWJh8AHaIYBBTqEggVNhTUFT4VkBVyC2weShT4Bx4YpBxGA2Ajgg+kKP5O2Cu2G+weUhAwHtIrvE4WJ0gEbiAEx+I6wAbmIoR2LjOIOhrLfDQSDnAtjktUMzYomPD78GQhTj5gc4ZzfDIaOOfvijusNzZxEHd+OVQ7q/fI86YLhG9eTMBjIgKP62rzGDk+NRRNNmyQCpPpdN5d5XgHxmpsSAJdp9MC0Z3hhg9oZg5GtGQSFnDL0dE02m5eCEB6WRxcYtmB0K7tQ5pmXDhbmkj02G3QkNP3yxxYukCYboqH7eEGz6PtnpzkWMpAKF7J6hTff+PQhzJoDFwqZjmKXvz78qa9gaWiRcR3/YHY4vfBjPIts7B3knE9tNa76/tO8eWwQrEgesG4JLTP/qj42bD8utJ98GPujZO09sRZprqLh6hiZoC8J6J8wx2jeIkrowxBdqtThL6rVc7KgO+Gypbkrh+VoKpBzuyjy4YYnT6Z25F2gpGXUou3nJqYOYznjAyPLZIAliOWHIHll0uHrox1oJLBf6mSgrWoz1lokPmAnJxDqiCEga/jRQa8+YSaoffGUqARtvd0E2C9vBik68GMtaWKQKCfbcW66rYP9ur01bE+utN3sWustgut+P9ZqjywR2GhdoK/J7j6/dnpNo1rdM1JcWaTgPzzVfXIt6+pxUYlYh+NYuaFwi7vS4TZZr1QU0Jw11HTbPs3jtliuUXnbgKWYd4G3+PRXVZ5X4tbL1/12xDM/9dVWLVRD0krQIXantsD0GbKTWnrSIdvuQcc+3fIkNcRUrdem2G5D0boK8NO8uEHQ2NfSIVo6TCP4bTm1Ri/c9NLdWltA0voZu1dMGMyR3Rhb00uJzH4/Jny9wFTOJF7sSsTMj7+WfOLNy8/BUthJV8PIyXF1y8iCwkVIj11Ux63I03aays3BRk9PUVnJsMZ4RgHEvsDmRB5Dos0rwnxJNLKfyeVHSkXjxdrFYUfIRQDGKUXjRXrEZcTfRNdHLcakR4NAfcXmwo5CQUhI9JBI2EBgzzfWjkVaQfPCXUn0QJ/JFNA9TwpH4spJsRdMqM1h3wtfE0K5zN4wl0y9wgTK5FxtSbbMr7gmf8LNC8rAXshcp8xpPJJ/i8yyyeVf3Fn0zo3N03IKuoHC112vUfnYUMm7fAE4HsfWXt1RFNqbwIx6dLC/+TZDGlbj1KjXEHZIMAD8SUBCWxrSldfPdRS1/fX0t3NV7dZW1x9TQTqo9NAzyVTO18fVnlbqsbP1fLiUY=", /* 3 */"FYiGAQqInfAGmKsv", /* 4 */1007922523, /* 5 */195569434, /* 6 */undefined, /* 7 */1967554067, /* 8 */1322412639, /* 9 */483210607, /* 10 */"D2/6+j1Vvc0jPQ==", /* 11 */-1226071446, /* 12 */466370398, /* 13 */1901891296, /* 14 */1355191139, /* 15 */-340919868, /* 16 */631047536, /* 17 */"kY8PBOeE", /* 18 */-2134825555, /* 19 */2071060400, /* 20 */1289204970, /* 21 */-967405336, /* 22 */611300621, /* 23 */67572446, /* 24 */-1123300232, /* 25 */"//jpUe+pxyDSqA==", /* 26 */1600429303, /* 27 */1002638259, /* 28 */-1314943612, /* 29 */392847268, /* 30 */"5Er01+y7yi7Emv51h43iAg==", /* 31 */-282349576, /* 32 */-59827951, /* 33 */1934489709, /* 34 */-98031018, /* 35 */"qQUdvo8SBpY2YqTuNFKvtQ==", /* 36 */"LtDuS2XAx1josdI546Ag6A==", /* 37 */619957552, /* 38 */"wSfNk8Z+woKPBfO9", /* 39 */"ek/zu5HAIUmC8Q==", /* 40 */-851800487, /* 41 */-720332988, /* 42 */-1522184915, /* 43 */1629514208, /* 44 */"VnBt6CKgWigdsKgYH4CWCQ==", /* 45 */-1989823039, /* 46 */"jVguoQ858JAgD4aD", /* 47 */"C0eMQhHZ0XHT6sUejKvfNw==", /* 48 */-477883560, /* 49 */-1860905231, /* 50 */"z+fdY4DY1LChSQ==", /* 51 */688479507, /* 52 */"Dwp3gmP2LHKEHwCihjYv0g==", /* 53 */1629590861, /* 54 */575460150, /* 55 */854846155, /* 56 */-1093688605, /* 57 */1287067289, /* 58 */"FmvNHgK1iTU=", /* 59 */-1025650345, /* 60 */1752687424, /* 61 */"bTBOmG5gHPhXUA==", /* 62 */-1236896407, /* 63 */"4OLVp+Azto7NBQ==", /* 64 */1003915137, /* 65 */"1hvebAD0pYUODIm8ChSl4w==", /* 66 */"nxs3jPj0C5QULIW8a1Sn0w==", /* 67 */-2092722545, /* 68 */1230476297, /* 69 */"loBNCYZxVulkoxY/d9M=", /* 70 */"Vt52QhBGgNIRbrblERo=", /* 71 */"NnOo5kohFrk=", /* 72 */295292233, /* 73 */-1290410416, /* 74 */227641692, /* 75 */"EP63ag/WxrveTsHaz3Fp7g==", /* 76 */-376047432, /* 77 */-1371518912, /* 78 */"SA9PlXslSq2fXFz3", /* 79 */"IPKcbRimjTofrpTb", /* 80 */1323874199, /* 81 */170204381, /* 82 */"E0+081VXuSw8vZwV", /* 83 */"s9IsZr/jIG4=", /* 84 */716696177, /* 85 */-662826603, /* 86 */210525997, /* 87 */"V6mZZgv7vQ/qmg==", /* 88 */284021408, /* 89 */"F3ax4tkdl7XWCtme", /* 90 */"eNQ/fL7sPhR0tA==", /* 91 */"2/rnVojLJb6WSk7W", /* 92 */-1656259837, /* 93 */1920738088, /* 94 */258917146, /* 95 */"l2cD3A9MiUUWxKhM", /* 96 */-1250225860, /* 97 */"4O/1e/nGzUo=", /* 98 */-480934334, /* 99 */"alxOpEE0SZxHDGiEdhw=", /* 100 */1325323163, /* 101 */"1UCC2NtxOuiDIA6onDM=", /* 102 */-1464247356, /* 103 */490618950, /* 104 */"JBRV/I9sENSsNQ==", /* 105 */"SyLQmSL2c4pUHkmKcjZvmg==", /* 106 */"zWqA3xVLrEYP3dNN6fY=", /* 107 */305236219, /* 108 */"xSAd2CVwWcgWQA==", /* 109 */701572361, /* 110 */"jXuaAOuY", /* 111 */927870405, /* 112 */true, /* 113 */"KsOsfzrTqlYNPQ==", /* 114 */1781732028, /* 115 */"s0ADx7JTFD/ZoFNp", /* 116 */"WP17l4YDLL+fEjCV", /* 117 */"AQKzfrMRI6q1Pg==", /* 118 */-1937140926, /* 119 */-1126246107, /* 120 */"gAk8kpt+", /* 121 */-1997460899, /* 122 */1541944152, /* 123 */"kdC6qTsh", /* 124 */-28533095, /* 125 */943556270, /* 126 */2135102392, /* 127 */1954657139, /* 128 */192138574, /* 129 */"zZoqDru70xY=", /* 130 */730405221, /* 131 */-543024036, /* 132 */-908853546, /* 133 */"kg/DtNMU2O3VfQ==", /* 134 */"C5apIiSu4dXSesXO7Vs=", /* 135 */45626642, /* 136 */1249011350, /* 137 */"h5HZ+feA3hjit1gojeA=", /* 138 */-1032548847, /* 139 */"7v7ciuIVK70=", /* 140 */"bHWQ5PBfF6vxNC5diM0=", /* 141 */-467259483, /* 142 */"RUNd18pyB+0=", /* 143 */-731201345, /* 144 */-1735028161, /* 145 */241758805, /* 146 */"mSJb6ZFxB4nLBg==", /* 147 */658450156, /* 148 */"eOYtGpyOfToqglYO", /* 149 */"9PjKYKrbLLfMSDnBoEk=", /* 150 */1617929869, /* 151 */"6wo5v5Mr", /* 152 */-1498101653, /* 153 */-748521493, /* 154 */"SLFWKVVjdOcoc7YeO4E=", /* 155 */"Ul15xU1Lbt8Fqw==", /* 156 */"O5+3Mh6paSE=", /* 157 */1666538591, /* 158 */1430778808, /* 159 */"UkI0SoPeYGp85mGa", /* 160 */1166754239, /* 161 */"G62hNADTt08=", /* 162 */1440138361, /* 163 */988811631, /* 164 */"Fid2s0TPa0U=", /* 165 */-51742132, /* 166 */"XZkFc2WPYQNghw==", /* 167 */935307222, /* 168 */1305174612, /* 169 */-2131057905, /* 170 */432546912, /* 171 */-449971897, /* 172 */-430629313, /* 173 */707606059, /* 174 */1288554009, /* 175 */-551761985, /* 176 */"FCdp60h/lZQ=", /* 177 */-1259287684, /* 178 */"e1BA6HZgRhhfsA==", /* 179 */1930519390, /* 180 */"XC8Oo37XZ02I/QFF/ModPw==", /* 181 */-1209153374, /* 182 */-32188679, /* 183 */-755030999, /* 184 */"FhSRlSc8Taxs1A==", /* 185 */"GgAsiBVwrp8LIN6oG9CzSTLA", /* 186 */"BK+mI+WYwRDCedgB94g=", /* 187 */"brSh3wNkKvSKnRcM", /* 188 */1254328946, /* 189 */-1758574041, /* 190 */"2h7gquEnB1ubzh55", /* 191 */"PnqlkR0OnYI2BqKTPv62YgUmLr6oSiLG", /* 192 */"Ji8o1aN1Nc2bXCI3krMVX67kLnU=", /* 193 */"M5Wu/QaNkxQDs/YvYuM=", /* 194 */-1223542774, /* 195 */"", /* 196 */"e0i0wC1IutcZuJggHpg=", /* 197 */"GXduy2pfUstpsQ==", /* 198 */"UTRMnDps", /* 199 */"ixs5bs/6wVXPzcfC", /* 200 */"Gj+HqyLXdHluwXxZa7E=", /* 201 */"/E8Eu3fV", /* 202 */"P2Lo5xySZQ4=", /* 203 */"nCkS0HJIUcA/WEMw", /* 204 */-315531579, /* 205 */"yrSgGz6MswQ=", /* 206 */"GqdU2VBx", /* 207 */-1456363561, /* 208 */"vrxBRLnFCXw=", /* 209 */"qlbK/edNisY=", /* 210 */"wnDK+OhBh8nFMA==", /* 211 */1534610130, /* 212 */"AIJgCpeeFCq/pz7atG55+g==", /* 213 */15203842, /* 214 */981596000, /* 215 */477054439, /* 216 */"I9q7TgW6", /* 217 */-116083656, /* 218 */"fCcNq7ocCXWJBT2d", /* 219 */"iTQ8rIhdOeSXdDWc", /* 220 */-1676239193, /* 221 */-186562806, /* 222 */-357195234, /* 223 */-322344111, /* 224 */-1720724768, /* 225 */"5+7RUqfHMMPIXlfxrWk34Q==", /* 226 */"Z4g2AImI", /* 227 */374693587, /* 228 */"6RDU5+5jgOg=", /* 229 */81627702, /* 230 */-940700954, /* 231 */-383352692, /* 232 */"Y4OBBx/zxG4apQ==", /* 233 */"NnmD0iZXvKMDL6WS", /* 234 */-1564543191, /* 235 */480322420, /* 236 */"4uHpWva26svrXw==", /* 237 */256885506, /* 238 */1314000921, /* 239 */"yovlHhDqnmEY3p0yGra6ww==", /* 240 */111984957, /* 241 */322342981, /* 242 */-1406668325, /* 243 */"QRdke3ntQKUvNWXNP0M=", /* 244 */1263436075, /* 245 */"Ykno0M27PS/xmBZxlYk=", /* 246 */-237122930, /* 247 */1473590638, /* 248 */-1571530254, /* 249 */-165248839, /* 250 */"gssetBdcRuw=", /* 251 */28260642, /* 252 */"APqXZtir", /* 253 */"YVpwtk3KRV5kyg==", /* 254 */99348973, /* 255 */"nOwZpKs1", /* 256 */1605564252, /* 257 */197524279, /* 258 */1399586042, /* 259 */702943369, /* 260 */640374254, /* 261 */"IhNnjy0FN5VgLVW7", /* 262 */-1213842507, /* 263 */-570357827, /* 264 */-1605538957, /* 265 */"idWGPZmsPec=", /* 266 */467970033, /* 267 */"TbpGDjeG", /* 268 */1351134625, /* 269 */"05WaFca8Jgy7lzF8oOw=", /* 270 */"wgNEj4UUM3U=", /* 271 */950713123, /* 272 */"IO6Noj4m+bP/Xg==", /* 273 */-955034769, /* 274 */-1235068589, /* 275 */939349849, /* 276 */1738584841, /* 277 */799404727, /* 278 */1288144827, /* 279 */1538681736, /* 280 */"U+h8cFHIOEBnuEXQXVhz4A==", /* 281 */39076253, /* 282 */"pZ866qlmH9q9rRQ2", /* 283 */"6IgbMbGZG2HM+DxTsc8=", /* 284 */1299930301, /* 285 */"zpbk8sKJzAH+uuYuzpvcZw==", /* 286 */1897167681, /* 287 */-1514630057, /* 288 */-533959981, /* 289 */-379026311, /* 290 */566827626, /* 291 */-1475244574, /* 292 */-438296259, /* 293 */"6ka3zd89", /* 294 */"vx3+pOo0", /* 295 */922538257, /* 296 */1427717708, /* 297 */738302799, /* 298 */2136620543, /* 299 */"xsLax7NTtO76YvTl", /* 300 */-644496113, /* 301 */-263014412, /* 302 */1741419264, /* 303 */2002004836, /* 304 */-1599992154, /* 305 */2014297548, /* 306 */"zZTLI+Wv7NRIdMLN+FzdtA==", /* 307 */1419970020, /* 308 */1662734054, /* 309 */563186886, /* 310 */"SYZDAnaO", /* 311 */"R8x4tAQkg+8gdK+U", /* 312 */-1679727318, /* 313 */-643586401, /* 314 */"PajGIApon/HdmNwA", /* 315 */"nJPSD4eCUhmSkSP5pWY=", /* 316 */-1574928010, /* 317 */-606266889, /* 318 */123615424, /* 319 */-614601907, /* 320 */"tqnhIdray26y+ztG", /* 321 */"VDBQiGIATng=", /* 322 */291319773, /* 323 */-1518395616, /* 324 */-1002126187, /* 325 */"AN6vSVuy094WYg==", /* 326 */2120056167, /* 327 */"T4B9+HKQQSh4oFIoTtBXeA==", /* 328 */"ZDBMmEVgUeg=", /* 329 */"tMkIccjoEiOwvy8TiYgPAd6JlxA=", /* 330 */"p5964pdm0t0YSg==", /* 331 */"Rj1by2tHRvt/Tw==", /* 332 */1660965965, /* 333 */-46166788, /* 334 */"nzDLyQtBsPgwU63XPqPoKA==", /* 335 */"qU6S1eC18y4=", /* 336 */"ZvyQbWuUQQRijEwUYnQ=", /* 337 */1821398531, /* 338 */-2012345256, /* 339 */-1690318599, /* 340 */2020099695, /* 341 */-37077790, /* 342 */1282360667, /* 343 */"aklPR5zbCm95+SJhTyk=", /* 344 */"VQwRvFwkbuQ=", /* 345 */-801114438, /* 346 */-1883283117, /* 347 */1986949871, /* 348 */"eyyFpAgk+m/Q9NxEvs07PL9U", /* 349 */-829051661, /* 350 */"g+k6ZqDaGi4=", /* 351 */"E/SFXyCkviQ=", /* 352 */-2060121211, /* 353 */-1828494213, /* 354 */-1534501764, /* 355 */-824142266, /* 356 */1904075023, /* 357 */-240451505, /* 358 */1102877994, /* 359 */555418943, /* 360 */1493856075, /* 361 */"ffBUaE4ga7g=", /* 362 */1681744012, /* 363 */499949701, /* 364 */679362194, /* 365 */1033405924, /* 366 */-1732212001, /* 367 */973119703, /* 368 */"jwEhiRN3SZs=", /* 369 */-1473269644, /* 370 */-864315762, /* 371 */459228512, /* 372 */14822597, /* 373 */"q3d2mqcOGrqpEdZu", /* 374 */335628932, /* 375 */-1561604645, /* 376 */"u+oCcacJ+II=", /* 377 */1171681475, /* 378 */124131077, /* 379 */-670697157]; +var BYTECODE = [0, 13, 0, 3852787432, 0, 12, 1, 1367678620, 47, 6, 3409, 1, 81, 0, 0, 0, 7, 2, 130174198, 42, 5, 6, 1, 7, 47, 8, 6540, 4, 99, 0, 0, 4, 1, 47, 14, 146, 1, 96, 1, 0, 1, 5, 61, 13, 13, 14, 13, 13, 12, 12, 12, 2, 13, 12, 2, 14, 3, 3853786501, 1, 9, 534411561, 1, 10, 172, 1, 11, 11, 42, 3, 8, 4, 5, 9, 10, 11, 0, 15, 4, 1007922522, 62, 12, 12, 12, 16, 13, 15, 12, 13, 13, 12, 13, 42, 15, 2, 1, 16, 0, 16, 5, 195569438, 42, 17, 2, 1, 16, 2, 16, 3, 3853786501, 1, 9, 3719064534, 1, 10, 186, 1, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 8, 18, 16, 4, 48, 16, 3, 15, 17, 18, 9, 14, 3, 16, 0, 14, 6, 0, 45, 14, 0, 94, 7, 1967554067, 0, 93, 8, 1322414876, 0, 92, 9, 483245829, 0, 91, 10, 955276479, 47, 88, 10345, 2, 10, 0, 0, 4, 19, 0, 20, 11, 3068871185, 61, 92, 92, 20, 92, 92, 91, 91, 92, 21, 92, 91, 0, 22, 7, 1967554067, 61, 92, 92, 22, 91, 92, 92, 91, 91, 23, 92, 91, 0, 24, 12, 466378709, 63, 91, 25, 21, 92, 92, 91, 24, 92, 92, 64, 25, 92, 93, 89, 92, 230, 92, 92, 91, 92, 91, 92, 65, 89, 92, 2, 89, 93, 92, 92, 92, 242, 91, 92, 1, 86, 26622879, 1, 90, 631449538, 66, 13, 14, 91, 1355177799, 92, 86, 263, 93, 90, 92, 90, 92, 1901875067, 91, 67, 92, 92, 90, 91, 91, 91, 92, 89, 90, 68, 91, 92, 86, 92, 92, 92, 86, 90, 91, 1, 87, 1370589624, 39, 3401, 0, 26, 15, 3954038588, 61, 92, 92, 26, 92, 92, 92, 92, 92, 23, 92, 92, 69, 91, 23, 91, 92, 92, 92, 27, 21, 91, 64, 27, 91, 93, 89, 92, 327, 92, 91, 91, 92, 91, 92, 65, 89, 92, 2, 89, 93, 91, 91, 92, 339, 92, 92, 1, 86, 1697115678, 1, 90, 1315587702, 66, 16, 17, 92, 1401718800, 91, 86, 360, 93, 90, 91, 90, 92, 631077196, 91, 67, 92, 92, 90, 92, 91, 92, 92, 89, 90, 68, 92, 91, 86, 91, 92, 91, 86, 90, 91, 1, 87, 929000916, 39, 3401, 1, 86, 2649943711, 1, 87, 3437119337, 39, 3401, 0, 28, 18, 2160136948, 68, 92, 91, 29, 92, 91, 92, 23, 28, 92, 61, 91, 92, 29, 91, 92, 92, 92, 91, 23, 92, 91, 69, 92, 23, 92, 91, 91, 92, 30, 21, 91, 64, 30, 92, 93, 89, 91, 442, 91, 92, 92, 92, 91, 91, 65, 89, 92, 2, 89, 93, 91, 92, 91, 454, 91, 91, 1, 86, 4292329314, 1, 90, 3633969146, 66, 19, 20, 92, 1289209000, 92, 86, 475, 93, 90, 92, 90, 92, 2071018000, 92, 67, 92, 91, 89, 92, 92, 91, 92, 90, 90, 68, 91, 92, 86, 92, 91, 92, 86, 90, 91, 1, 87, 3213871207, 39, 3401, 1, 86, 4205696425, 1, 87, 1723742081, 39, 3401, 0, 31, 21, 3327582015, 68, 92, 91, 32, 92, 91, 91, 23, 31, 92, 61, 92, 91, 32, 92, 91, 91, 92, 91, 23, 91, 92, 69, 92, 23, 91, 92, 92, 91, 33, 21, 92, 64, 33, 92, 93, 89, 92, 557, 91, 92, 92, 92, 91, 92, 65, 89, 91, 0, 89, 93, 91, 91, 92, 569, 91, 92, 1, 86, 4030170073, 1, 90, 2298076033, 66, 22, 23, 92, 67578454, 91, 86, 590, 93, 90, 91, 90, 92, 611254277, 91, 67, 91, 92, 90, 91, 91, 91, 92, 89, 90, 68, 91, 92, 86, 92, 91, 92, 86, 90, 91, 1, 87, 504257279, 39, 3401, 1, 86, 4216224548, 1, 87, 3311957588, 39, 3401, 0, 34, 24, 3171638651, 66, 25, 26, 91, 1600399913, 91, 34, 642, 93, 35, 92, 23, 92, 3710822916, 91, 61, 92, 92, 35, 92, 92, 91, 92, 91, 23, 91, 92, 69, 91, 23, 91, 91, 92, 91, 36, 21, 92, 64, 36, 91, 93, 89, 91, 677, 91, 92, 92, 92, 92, 92, 65, 89, 92, 2, 89, 93, 92, 92, 92, 689, 92, 92, 1, 86, 3880399545, 1, 90, 3196635889, 66, 27, 28, 92, 2980012693, 91, 86, 710, 93, 90, 91, 90, 92, 1002647101, 91, 67, 91, 91, 90, 91, 92, 92, 91, 89, 90, 68, 92, 92, 86, 91, 92, 91, 86, 90, 92, 1, 87, 3168149095, 39, 3401, 1, 86, 2851958169, 1, 87, 299757609, 39, 3401, 0, 37, 29, 392843692, 66, 30, 31, 92, 4012587269, 91, 37, 762, 93, 38, 92, 23, 92, 876633988, 91, 61, 91, 92, 38, 92, 92, 91, 91, 91, 23, 91, 92, 69, 91, 23, 91, 92, 91, 91, 39, 21, 91, 64, 39, 91, 93, 89, 91, 797, 92, 91, 92, 91, 91, 91, 65, 89, 92, 0, 89, 93, 92, 91, 91, 809, 92, 92, 62, 91, 92, 92, 89, 92, 89, 92, 92, 91, 92, 91, 1, 86, 2435468286, 1, 90, 2851054374, 66, 32, 33, 92, 1934460914, 92, 86, 842, 93, 90, 92, 90, 91, 4235148848, 91, 70, 92, 92, 92, 89, 91, 91, 91, 91, 91, 90, 90, 68, 92, 91, 86, 91, 91, 91, 86, 90, 92, 1, 87, 3642616296, 39, 3401, 1, 86, 2942368288, 1, 87, 2949860017, 39, 3401, 0, 40, 34, 4196954464, 68, 92, 92, 41, 92, 92, 92, 23, 40, 92, 61, 91, 91, 41, 92, 92, 92, 91, 92, 23, 92, 92, 69, 91, 23, 92, 91, 92, 91, 42, 21, 91, 64, 42, 92, 93, 89, 91, 926, 91, 91, 91, 92, 92, 91, 65, 89, 91, 0, 89, 93, 92, 92, 92, 938, 91, 92, 1, 86, 970088549, 1, 90, 2702710989, 66, 35, 36, 91, 3235098203, 91, 86, 959, 93, 90, 92, 90, 92, 2672455708, 91, 67, 92, 91, 90, 92, 92, 91, 92, 89, 90, 68, 91, 91, 86, 92, 91, 92, 86, 90, 92, 1, 87, 4156934734, 39, 3401, 1, 86, 2255000428, 1, 87, 2425049246, 39, 3401, 0, 43, 37, 619944180, 68, 91, 91, 44, 92, 92, 92, 23, 43, 92, 61, 92, 92, 44, 92, 92, 92, 91, 91, 23, 91, 92, 69, 91, 23, 92, 91, 92, 92, 45, 21, 91, 64, 45, 91, 93, 89, 91, 1041, 91, 91, 91, 92, 92, 91, 65, 89, 92, 2, 89, 93, 92, 92, 91, 1053, 92, 92, 1, 86, 1604319737, 1, 90, 4141453665, 66, 38, 39, 91, 3552659154, 91, 86, 1074, 93, 90, 91, 90, 92, 4260777284, 91, 67, 92, 91, 90, 92, 91, 91, 91, 89, 90, 68, 91, 91, 86, 91, 91, 92, 86, 90, 91, 1, 87, 174820235, 39, 3401, 1, 86, 729472054, 1, 87, 1188466239, 39, 3401, 0, 46, 40, 3443168829, 68, 91, 91, 47, 91, 91, 91, 23, 46, 91, 61, 91, 91, 47, 91, 92, 91, 91, 92, 23, 91, 92, 69, 91, 23, 92, 91, 92, 91, 48, 21, 92, 64, 48, 92, 93, 89, 91, 1156, 92, 92, 92, 92, 91, 91, 65, 89, 92, 0, 89, 93, 92, 91, 91, 1168, 92, 91, 62, 92, 92, 91, 89, 91, 89, 91, 91, 92, 91, 92, 1, 86, 1990590530, 1, 90, 3731187722, 66, 41, 42, 92, 2772782067, 91, 86, 1201, 93, 90, 91, 90, 92, 3574608490, 91, 70, 91, 91, 91, 90, 91, 92, 92, 92, 91, 89, 90, 68, 92, 92, 86, 91, 92, 92, 86, 90, 91, 1, 87, 1973739711, 39, 3401, 1, 86, 2047439572, 1, 87, 731085635, 39, 3401, 0, 49, 43, 1629493026, 66, 44, 45, 91, 2305125725, 92, 49, 1255, 93, 50, 91, 23, 91, 3307532574, 91, 61, 91, 92, 50, 91, 92, 91, 92, 91, 23, 92, 91, 69, 91, 23, 91, 91, 91, 92, 51, 21, 92, 64, 51, 91, 93, 89, 91, 1290, 92, 92, 92, 91, 92, 92, 65, 89, 91, 0, 89, 93, 92, 92, 92, 1302, 91, 92, 1, 86, 66454138, 1, 90, 347854818, 66, 46, 47, 92, 44680895, 91, 86, 1323, 93, 90, 91, 90, 92, 443098132, 92, 67, 92, 92, 89, 92, 92, 92, 92, 90, 90, 68, 92, 91, 86, 92, 91, 92, 86, 90, 92, 1, 87, 1536966623, 39, 3401, 1, 86, 3529756505, 1, 87, 451380270, 39, 3401, 0, 52, 48, 3817095851, 66, 49, 50, 92, 2697172665, 91, 52, 1375, 93, 53, 91, 23, 91, 2434035895, 91, 61, 92, 91, 53, 91, 91, 91, 92, 91, 23, 91, 92, 69, 91, 23, 91, 91, 91, 92, 54, 21, 91, 64, 54, 91, 93, 89, 91, 1410, 92, 91, 91, 91, 92, 92, 65, 89, 91, 1, 89, 93, 92, 91, 91, 1422, 92, 91, 62, 92, 91, 92, 89, 92, 89, 92, 91, 92, 91, 91, 1, 86, 1528227100, 1, 90, 1406241972, 66, 51, 52, 92, 1431116895, 91, 86, 1455, 93, 90, 91, 90, 91, 688461533, 92, 70, 92, 91, 92, 90, 92, 91, 91, 91, 91, 89, 90, 68, 91, 91, 86, 91, 91, 92, 86, 90, 91, 1, 87, 774591219, 39, 3401, 1, 86, 3303396209, 1, 87, 1810282610, 39, 3401, 0, 55, 53, 1629595675, 68, 92, 91, 56, 92, 91, 91, 23, 55, 92, 61, 91, 91, 56, 91, 92, 92, 92, 91, 23, 91, 91, 69, 91, 23, 91, 92, 91, 91, 57, 21, 92, 64, 57, 91, 93, 89, 92, 1539, 92, 92, 91, 91, 91, 91, 65, 89, 91, 0, 89, 93, 91, 91, 92, 1551, 92, 91, 62, 91, 91, 92, 89, 92, 89, 92, 92, 92, 91, 91, 1, 86, 2039589757, 1, 90, 2261992405, 66, 54, 55, 92, 854833749, 91, 86, 1584, 93, 90, 91, 90, 92, 575427332, 91, 70, 92, 91, 91, 89, 91, 91, 91, 92, 91, 90, 90, 68, 91, 91, 86, 91, 92, 92, 86, 90, 91, 1, 87, 1440467719, 39, 3401, 1, 86, 1549011077, 1, 87, 507532900, 39, 3401, 0, 58, 56, 3201239327, 68, 91, 91, 59, 91, 91, 91, 23, 58, 91, 61, 91, 91, 59, 92, 91, 92, 92, 92, 23, 91, 91, 69, 92, 23, 92, 92, 91, 92, 60, 21, 92, 64, 60, 91, 93, 89, 92, 1668, 91, 92, 91, 92, 91, 92, 65, 89, 92, 0, 89, 93, 91, 92, 91, 1680, 92, 92, 1, 86, 4281850864, 1, 90, 670376840, 66, 57, 58, 92, 1934978412, 91, 86, 1701, 93, 90, 92, 90, 91, 1287113848, 92, 67, 91, 91, 89, 92, 91, 92, 92, 90, 90, 68, 92, 92, 86, 91, 91, 92, 86, 90, 91, 1, 87, 2233315747, 39, 3401, 1, 86, 3206466515, 1, 87, 1969026035, 39, 3401, 0, 61, 59, 3269305254, 66, 60, 61, 92, 1013672829, 91, 61, 1753, 93, 62, 91, 23, 92, 1752665988, 92, 61, 91, 92, 62, 91, 91, 92, 91, 91, 23, 92, 91, 69, 91, 23, 92, 91, 91, 91, 63, 21, 91, 64, 63, 91, 93, 89, 92, 1788, 92, 91, 91, 92, 91, 91, 65, 89, 91, 1, 89, 93, 92, 92, 91, 1800, 92, 92, 1, 86, 3220443723, 1, 90, 2788281043, 66, 62, 63, 91, 3535399769, 91, 86, 1821, 93, 90, 92, 90, 91, 3058095326, 92, 67, 92, 91, 89, 91, 91, 92, 91, 90, 90, 68, 91, 91, 86, 91, 92, 91, 86, 90, 92, 1, 87, 2690722622, 39, 3401, 1, 86, 1357621300, 1, 87, 2320651308, 39, 3401, 0, 64, 64, 1003934380, 68, 91, 92, 65, 91, 91, 91, 23, 64, 92, 61, 92, 92, 65, 92, 92, 92, 92, 92, 23, 91, 91, 69, 92, 23, 92, 92, 92, 92, 66, 21, 91, 64, 66, 92, 93, 89, 92, 1903, 92, 91, 92, 92, 92, 91, 65, 89, 92, 2, 89, 93, 92, 92, 92, 1915, 92, 92, 1, 86, 2133103132, 1, 90, 2254547684, 66, 65, 66, 92, 608867869, 92, 86, 1936, 93, 90, 92, 90, 92, 1438648911, 92, 67, 91, 91, 90, 92, 92, 91, 92, 89, 90, 68, 92, 91, 86, 91, 92, 92, 86, 90, 91, 1, 87, 1381096734, 39, 3401, 1, 86, 541500493, 1, 87, 1422537573, 39, 3401, 0, 67, 67, 2202241137, 66, 68, 69, 91, 2217351742, 91, 67, 1988, 93, 68, 91, 23, 91, 1230459488, 91, 61, 91, 92, 68, 91, 91, 92, 92, 92, 23, 92, 91, 69, 92, 23, 91, 92, 91, 91, 69, 21, 91, 64, 69, 92, 93, 89, 92, 2023, 91, 91, 92, 92, 92, 91, 65, 89, 92, 0, 89, 93, 92, 92, 92, 2035, 92, 91, 1, 86, 4051820226, 1, 90, 3402630682, 66, 70, 71, 92, 476916882, 91, 86, 2056, 93, 90, 92, 90, 92, 3295957394, 91, 67, 91, 92, 89, 91, 92, 92, 92, 90, 90, 68, 91, 91, 86, 92, 92, 91, 86, 90, 91, 1, 87, 2069414544, 39, 3401, 1, 86, 3624441227, 1, 87, 412377078, 39, 3401, 0, 70, 72, 295280261, 66, 73, 74, 92, 227637477, 91, 70, 2108, 93, 71, 91, 23, 91, 3004504331, 92, 61, 91, 92, 71, 92, 92, 91, 91, 91, 23, 91, 91, 69, 92, 23, 91, 92, 91, 92, 72, 21, 92, 64, 72, 91, 93, 89, 91, 2143, 91, 91, 92, 92, 91, 91, 65, 89, 91, 2, 89, 93, 91, 92, 92, 2155, 92, 91, 62, 92, 92, 92, 89, 91, 89, 91, 91, 91, 91, 92, 1, 86, 471101695, 1, 90, 626186327, 66, 75, 76, 92, 3918871180, 92, 86, 2188, 93, 90, 91, 90, 91, 1878660662, 91, 70, 92, 91, 91, 90, 91, 91, 92, 91, 92, 89, 90, 68, 91, 92, 86, 91, 91, 91, 86, 90, 91, 1, 87, 2474534037, 39, 3401, 1, 86, 2616927068, 1, 87, 3940106906, 39, 3401, 0, 73, 77, 2923441844, 68, 91, 92, 74, 91, 91, 92, 23, 73, 91, 61, 91, 91, 74, 91, 91, 91, 92, 91, 23, 91, 92, 69, 92, 23, 92, 92, 91, 92, 75, 21, 92, 64, 75, 91, 93, 89, 92, 2272, 91, 92, 91, 91, 91, 91, 65, 89, 92, 0, 89, 93, 92, 92, 91, 2284, 92, 92, 62, 91, 91, 92, 89, 92, 89, 92, 92, 92, 92, 92, 1, 86, 1183927951, 1, 90, 1834182119, 66, 78, 79, 91, 689196659, 92, 86, 2317, 93, 90, 91, 90, 91, 1340802137, 92, 70, 92, 92, 92, 90, 92, 91, 91, 92, 92, 89, 90, 68, 91, 92, 86, 91, 92, 92, 86, 90, 91, 1, 87, 1345047184, 39, 3401, 1, 86, 4098264415, 1, 87, 2873449382, 39, 3401, 0, 76, 80, 1323883717, 66, 81, 82, 91, 2793926900, 91, 76, 2371, 93, 77, 92, 23, 91, 170230140, 91, 61, 92, 91, 77, 91, 91, 91, 91, 91, 23, 91, 92, 69, 91, 23, 91, 91, 91, 92, 78, 21, 92, 64, 78, 91, 93, 89, 92, 2406, 91, 92, 91, 91, 92, 91, 65, 89, 92, 2, 89, 93, 91, 91, 92, 2418, 92, 91, 1, 86, 3333401347, 1, 90, 2683207483, 66, 83, 84, 91, 716679155, 91, 86, 2439, 93, 90, 91, 90, 92, 3480634848, 91, 67, 92, 92, 90, 92, 92, 92, 91, 89, 90, 68, 92, 91, 86, 91, 91, 92, 86, 90, 91, 1, 87, 3175955268, 39, 3401, 1, 86, 2480656649, 1, 87, 1017566342, 39, 3401, 0, 79, 85, 3632157733, 68, 92, 92, 80, 92, 92, 92, 23, 79, 91, 61, 92, 92, 80, 91, 92, 92, 91, 92, 23, 92, 92, 69, 92, 23, 92, 92, 92, 91, 81, 21, 92, 64, 81, 91, 93, 89, 91, 2521, 92, 92, 92, 91, 92, 92, 65, 89, 91, 2, 89, 93, 91, 91, 91, 2533, 92, 91, 62, 91, 91, 91, 89, 92, 89, 92, 92, 92, 92, 92, 1, 86, 2054801836, 1, 90, 3803963716, 66, 86, 87, 91, 1862945201, 92, 86, 2566, 93, 90, 91, 90, 92, 210508722, 91, 70, 91, 91, 92, 89, 91, 91, 91, 91, 91, 90, 90, 68, 92, 91, 86, 92, 91, 92, 86, 90, 92, 1, 87, 1935067558, 39, 3401, 1, 86, 1629331357, 1, 87, 836147959, 39, 3401, 0, 82, 88, 283989827, 66, 89, 90, 91, 3188243543, 91, 82, 2620, 93, 83, 91, 23, 92, 484717778, 91, 61, 92, 91, 83, 92, 92, 92, 92, 92, 23, 91, 92, 69, 92, 23, 92, 92, 91, 92, 84, 21, 91, 64, 84, 92, 93, 89, 91, 2655, 91, 92, 92, 91, 91, 91, 65, 89, 91, 1, 89, 93, 92, 91, 91, 2667, 92, 92, 1, 86, 1568024014, 1, 90, 917962198, 66, 91, 92, 92, 2638738698, 91, 86, 2688, 93, 90, 92, 90, 91, 3875625392, 92, 67, 91, 92, 90, 91, 91, 91, 92, 89, 90, 68, 92, 92, 86, 91, 92, 91, 86, 90, 91, 1, 87, 3152230524, 39, 3401, 1, 86, 841286482, 1, 87, 2761370508, 39, 3401, 1, 86, 2686626304, 1, 87, 2030858752, 39, 3401, 61, 91, 92, 9, 92, 92, 92, 91, 91, 5, 92, 91, 1, 4, 2995, 55, 1, 85, 17291, 68, 91, 91, 21, 92, 91, 92, 21, 85, 92, 1, 86, 2085540908, 1, 87, 1566592500, 39, 3401, 55, 1, 4, 2796, 1, 85, 4730, 66, 93, 94, 92, 258930306, 92, 85, 2788, 93, 21, 92, 21, 92, 1920775178, 91, 1, 86, 3755585682, 1, 87, 3203885458, 39, 3401, 1, 21, 25644, 0, 95, 6, 0, 45, 95, 59, 2843, 4, 5, 3027, 54, 3243, 3, 0, 6, 7, 1967554067, 1, 85, 7022, 66, 95, 96, 91, 3044787371, 91, 85, 2835, 93, 21, 91, 21, 91, 1862833172, 92, 1, 86, 982044977, 1, 87, 329665267, 39, 3401, 1, 21, 43484, 2, 11, 3, 3853786501, 1, 16, 3719064534, 1, 17, 186, 1, 85, 33975, 66, 97, 98, 91, 3814001182, 91, 85, 2874, 93, 21, 91, 21, 91, 400942501, 92, 1, 86, 2882311254, 1, 87, 1659211182, 39, 3401, 3, 14, 0, 47, 15, 6540, 4, 99, 0, 0, 4, 2, 1, 85, 30705, 66, 99, 100, 92, 1325280659, 91, 85, 2912, 93, 21, 92, 21, 92, 3925255110, 92, 1, 86, 164015098, 1, 87, 3236459530, 39, 3401, 42, 10, 15, 4, 14, 16, 17, 18, 8, 95, 3, 10, 61, 92, 91, 95, 91, 92, 92, 91, 92, 5, 91, 92, 1, 85, 59730, 68, 91, 92, 21, 91, 92, 91, 21, 85, 91, 1, 86, 2501773241, 1, 87, 1949503563, 39, 3401, 1, 4, 3073, 55, 1, 85, 20284, 66, 101, 102, 91, 2830710212, 92, 85, 2987, 93, 21, 92, 21, 92, 3233234453, 91, 1, 86, 4155795076, 1, 87, 3600662428, 39, 3401, 1, 21, 12024, 45, 5, 9, 11, 12, 13, 58, 4, 1, 85, 40715, 68, 92, 91, 21, 92, 92, 91, 21, 85, 92, 1, 86, 2651533838, 1, 87, 2133125654, 39, 3401, 1, 21, 46632, 46, 5, 0, 95, 4, 1007922522, 68, 92, 91, 9, 92, 91, 92, 0, 95, 92, 55, 1, 85, 25030, 66, 103, 104, 92, 168877135, 92, 85, 3065, 93, 21, 91, 21, 92, 490606449, 92, 1, 86, 1798803766, 1, 87, 572033742, 39, 3401, 1, 21, 56148, 45, 5, 71, 91, 92, 3604091004, 92, 95, 92, 92, 91, 91, 91, 6, 92, 105, 0, 64, 95, 92, 93, 89, 91, 3106, 92, 92, 91, 91, 92, 92, 65, 89, 92, 0, 89, 93, 91, 91, 92, 3118, 91, 91, 62, 92, 92, 92, 89, 91, 89, 91, 92, 91, 91, 92, 1, 86, 2714065750, 1, 90, 3862475723, 66, 106, 107, 92, 305231156, 92, 86, 3151, 93, 90, 92, 90, 91, 1723519821, 92, 70, 92, 92, 91, 90, 91, 92, 92, 92, 92, 89, 90, 68, 92, 92, 86, 91, 92, 92, 86, 90, 92, 1, 87, 2074540898, 39, 3401, 1, 85, 31483, 68, 92, 92, 21, 92, 92, 92, 21, 85, 91, 1, 86, 409153096, 1, 87, 4048353240, 39, 3401, 1, 85, 10531, 66, 108, 109, 91, 701619420, 92, 85, 3217, 93, 21, 91, 21, 91, 452554679, 91, 1, 86, 4271581563, 1, 87, 3753177733, 39, 3401, 1, 18, 8, 42, 8, 15, 4, 14, 16, 17, 18, 44, 95, 7, 1, 8, 46, 95, 1, 21, 34343, 1, 16, 2543968747, 1, 17, 208, 1, 18, 7, 1, 85, 30305, 66, 110, 111, 91, 927889303, 92, 85, 3273, 93, 21, 91, 21, 92, 4069360602, 91, 1, 86, 1760314066, 1, 87, 566614866, 39, 3401, 1, 18, 8, 42, 12, 15, 4, 14, 16, 17, 18, 0, 13, 112, 0, 1, 85, 3592, 66, 113, 114, 92, 1781790738, 91, 85, 3314, 93, 21, 91, 21, 92, 926740266, 92, 1, 86, 1500508513, 1, 87, 809577123, 39, 3401, 55, 1, 85, 4904, 66, 115, 116, 92, 46758767, 91, 85, 3341, 93, 21, 92, 21, 92, 3852985445, 91, 1, 86, 1350915532, 1, 87, 162188884, 39, 3401, 2, 7, 117, 3273232147, 1, 16, 1136099669, 1, 17, 195, 1, 85, 20058, 68, 92, 92, 21, 92, 92, 92, 21, 85, 91, 1, 86, 453578123, 1, 87, 4062582421, 39, 3401, 1, 85, 9326, 68, 91, 91, 21, 91, 91, 91, 21, 85, 91, 1, 86, 2755750424, 1, 87, 1701753832, 39, 3401, 42, 86, 88, 2, 87, 86, 58, 86, 0, 78, 7, 1967554067, 0, 75, 118, 2357838832, 0, 77, 119, 3168678548, 0, 76, 120, 1540317242, 47, 72, 10649, 2, 12, 0, 0, 4, 15, 0, 16, 121, 2297504959, 61, 76, 75, 16, 75, 75, 75, 76, 75, 17, 75, 76, 0, 18, 7, 1967554067, 61, 75, 76, 18, 76, 76, 76, 75, 76, 19, 75, 76, 0, 20, 122, 1541934937, 63, 76, 21, 17, 75, 76, 75, 20, 76, 76, 64, 21, 76, 77, 73, 75, 3493, 75, 76, 75, 76, 76, 75, 65, 73, 75, 2, 73, 77, 75, 75, 76, 3505, 75, 75, 1, 70, 607039633, 1, 74, 517576875, 66, 123, 124, 76, 4266446118, 75, 70, 3526, 77, 74, 75, 74, 76, 2474435404, 75, 67, 75, 75, 74, 75, 75, 75, 75, 73, 74, 68, 75, 76, 70, 75, 76, 76, 70, 74, 75, 1, 71, 3171595688, 39, 6532, 0, 22, 125, 943584686, 61, 75, 75, 22, 75, 75, 75, 75, 76, 19, 75, 76, 69, 76, 19, 75, 76, 75, 75, 23, 17, 75, 64, 23, 76, 77, 73, 75, 3590, 75, 75, 75, 76, 75, 75, 65, 73, 76, 2, 73, 77, 75, 75, 76, 3602, 76, 75, 1, 70, 919884090, 1, 74, 2234330386, 66, 126, 127, 76, 1954629096, 75, 70, 3623, 77, 74, 75, 74, 76, 2135152586, 76, 67, 76, 76, 73, 76, 75, 76, 75, 74, 74, 68, 76, 75, 70, 76, 75, 76, 70, 74, 76, 1, 71, 330511343, 39, 6532, 1, 70, 3731589585, 1, 71, 3046273044, 39, 6532, 0, 24, 128, 192103434, 68, 76, 76, 25, 76, 76, 76, 19, 24, 75, 61, 75, 76, 25, 76, 75, 76, 75, 76, 19, 76, 76, 69, 75, 19, 76, 75, 76, 75, 26, 17, 75, 64, 26, 75, 77, 73, 75, 3705, 76, 76, 76, 75, 75, 76, 65, 73, 75, 0, 73, 77, 75, 75, 76, 3717, 75, 75, 62, 75, 76, 75, 73, 75, 73, 76, 76, 76, 76, 76, 1, 70, 3709239857, 1, 74, 2394793561, 66, 129, 130, 76, 730447287, 75, 70, 3750, 77, 74, 75, 74, 76, 2159997845, 76, 70, 75, 76, 75, 74, 75, 76, 75, 76, 75, 73, 74, 68, 76, 75, 70, 75, 75, 76, 70, 74, 76, 1, 71, 261845204, 39, 6532, 1, 70, 531400738, 1, 71, 1546351786, 39, 6532, 0, 27, 131, 3751959247, 66, 132, 133, 76, 2928734199, 75, 27, 3804, 77, 28, 76, 19, 76, 3386075721, 76, 61, 76, 76, 28, 76, 76, 75, 75, 76, 19, 76, 76, 69, 76, 19, 75, 76, 75, 75, 29, 17, 75, 64, 29, 75, 77, 73, 75, 3839, 75, 76, 76, 75, 75, 75, 65, 73, 76, 1, 73, 77, 75, 76, 76, 3851, 75, 76, 1, 70, 3240177355, 1, 74, 1451864755, 66, 134, 135, 75, 45657057, 76, 70, 3872, 77, 74, 75, 74, 75, 3051601112, 76, 67, 76, 75, 73, 76, 75, 76, 75, 74, 74, 68, 76, 76, 70, 76, 76, 76, 70, 74, 75, 1, 71, 576976343, 39, 6532, 1, 70, 3806636484, 1, 71, 2426341535, 39, 6532, 0, 30, 136, 1249035653, 66, 137, 138, 76, 3262391813, 75, 30, 3924, 77, 31, 76, 19, 76, 3026553766, 76, 61, 76, 76, 31, 76, 76, 75, 76, 76, 19, 76, 76, 69, 75, 19, 75, 75, 75, 76, 32, 17, 75, 64, 32, 75, 77, 73, 76, 3959, 76, 76, 75, 76, 76, 76, 65, 73, 76, 0, 73, 77, 75, 76, 76, 3971, 75, 76, 1, 70, 597255647, 1, 74, 4051642951, 66, 139, 140, 75, 3643364505, 75, 70, 3992, 77, 74, 75, 74, 75, 2447836304, 75, 67, 75, 75, 73, 75, 75, 76, 75, 74, 74, 68, 75, 76, 70, 76, 76, 76, 70, 74, 75, 1, 71, 2364543666, 39, 6532, 1, 70, 2614451461, 1, 71, 3619086347, 39, 6532, 0, 33, 141, 3827713793, 68, 75, 76, 34, 75, 76, 75, 19, 33, 75, 61, 75, 76, 34, 76, 76, 76, 75, 76, 19, 76, 75, 69, 76, 19, 75, 76, 75, 76, 35, 17, 75, 64, 35, 76, 77, 73, 76, 4074, 75, 75, 75, 75, 75, 76, 65, 73, 76, 2, 73, 77, 75, 75, 75, 4086, 75, 76, 62, 76, 75, 76, 73, 76, 73, 75, 76, 75, 76, 75, 1, 70, 130406115, 1, 74, 1001156667, 66, 142, 143, 76, 3563754392, 76, 70, 4119, 77, 74, 75, 74, 76, 4166680014, 75, 70, 76, 76, 76, 74, 75, 76, 76, 76, 76, 73, 74, 68, 76, 75, 70, 75, 75, 75, 70, 74, 76, 1, 71, 4276300129, 39, 6532, 1, 70, 3920410414, 1, 71, 2387175318, 39, 6532, 0, 36, 144, 2559959881, 68, 75, 75, 37, 75, 75, 75, 19, 36, 75, 61, 76, 76, 37, 76, 75, 75, 76, 76, 19, 75, 76, 69, 75, 19, 75, 75, 75, 76, 38, 17, 76, 64, 38, 76, 77, 73, 75, 4203, 76, 75, 76, 75, 76, 75, 65, 73, 76, 1, 73, 77, 76, 75, 75, 4215, 76, 76, 62, 76, 76, 75, 73, 76, 73, 75, 76, 76, 76, 76, 1, 70, 518541037, 1, 74, 4217404133, 66, 145, 146, 75, 3194113144, 76, 70, 4248, 77, 74, 75, 74, 75, 241755818, 75, 70, 75, 75, 76, 73, 76, 75, 76, 76, 75, 74, 74, 68, 76, 76, 70, 76, 75, 76, 70, 74, 76, 1, 71, 254697262, 39, 6532, 1, 70, 538551959, 1, 71, 1863209001, 39, 6532, 0, 39, 147, 658479336, 66, 148, 149, 76, 3232011418, 76, 39, 4302, 77, 40, 76, 19, 75, 1529459530, 76, 61, 76, 76, 40, 76, 75, 76, 76, 75, 19, 76, 75, 69, 75, 19, 75, 76, 76, 76, 41, 17, 76, 64, 41, 76, 77, 73, 76, 4337, 76, 76, 75, 76, 76, 75, 65, 73, 76, 2, 73, 77, 76, 76, 76, 4349, 75, 76, 62, 75, 76, 75, 73, 75, 73, 76, 76, 76, 76, 76, 1, 70, 748668897, 1, 74, 626431001, 66, 150, 151, 75, 3871716221, 76, 70, 4382, 77, 74, 75, 74, 76, 1617904389, 76, 70, 75, 76, 76, 73, 76, 75, 75, 75, 76, 74, 74, 68, 76, 75, 70, 76, 75, 75, 70, 74, 75, 1, 71, 2579235230, 39, 6532, 1, 70, 3300980104, 1, 71, 1966934932, 39, 6532, 0, 42, 152, 2796869561, 66, 153, 154, 76, 959746208, 75, 42, 4436, 77, 43, 76, 19, 76, 3546472165, 76, 61, 76, 76, 43, 75, 75, 75, 76, 76, 19, 76, 76, 69, 76, 19, 76, 76, 76, 75, 44, 17, 75, 64, 44, 75, 77, 73, 76, 4471, 75, 75, 76, 76, 76, 75, 65, 73, 75, 0, 73, 77, 76, 76, 76, 4483, 75, 76, 62, 75, 75, 75, 73, 75, 73, 76, 76, 75, 76, 75, 1, 70, 3443160381, 1, 74, 936505685, 66, 155, 156, 75, 2136078730, 76, 70, 4516, 77, 74, 75, 74, 76, 676786114, 76, 70, 76, 76, 76, 74, 76, 75, 75, 75, 75, 73, 74, 68, 75, 75, 70, 75, 76, 76, 70, 74, 75, 1, 71, 1776761046, 39, 6532, 1, 70, 1400919777, 1, 71, 956505655, 39, 6532, 0, 45, 157, 1666525091, 68, 75, 75, 46, 75, 75, 75, 19, 45, 75, 61, 76, 76, 46, 75, 76, 75, 75, 76, 19, 75, 75, 69, 75, 19, 76, 76, 76, 76, 47, 17, 76, 64, 47, 76, 77, 73, 76, 4600, 76, 76, 76, 75, 76, 76, 65, 73, 76, 2, 73, 77, 75, 76, 76, 4612, 76, 76, 62, 75, 75, 75, 73, 76, 73, 75, 76, 76, 75, 76, 1, 70, 3759087651, 1, 74, 3162983451, 66, 158, 159, 75, 2861075032, 75, 70, 4645, 77, 74, 76, 74, 76, 1430753623, 75, 70, 75, 75, 75, 74, 76, 76, 75, 75, 75, 73, 74, 68, 76, 75, 70, 75, 75, 75, 70, 74, 76, 1, 71, 3237955432, 39, 6532, 1, 70, 1702512854, 1, 71, 3354266430, 39, 6532, 0, 48, 160, 1166765511, 68, 76, 76, 49, 75, 75, 76, 19, 48, 75, 61, 76, 76, 49, 75, 76, 75, 75, 76, 19, 76, 76, 69, 76, 19, 76, 76, 75, 76, 50, 17, 76, 64, 50, 75, 77, 73, 76, 4729, 76, 76, 76, 76, 76, 75, 65, 73, 76, 0, 73, 77, 75, 76, 75, 4741, 75, 76, 1, 70, 3003995536, 1, 74, 2407891336, 66, 161, 162, 76, 1440150728, 76, 70, 4762, 77, 74, 75, 74, 75, 2355789502, 75, 67, 75, 75, 73, 75, 75, 75, 75, 74, 74, 68, 75, 75, 70, 75, 75, 75, 70, 74, 75, 1, 71, 2834797311, 39, 6532, 1, 70, 3464751485, 1, 71, 1447822809, 39, 6532, 0, 51, 163, 988855620, 66, 164, 165, 76, 4243198206, 76, 51, 4814, 77, 52, 76, 19, 76, 2231158027, 75, 61, 75, 76, 52, 75, 76, 76, 75, 76, 19, 75, 76, 69, 76, 19, 76, 76, 75, 76, 53, 17, 75, 64, 53, 75, 77, 73, 75, 4849, 76, 76, 75, 76, 75, 76, 65, 73, 76, 1, 73, 77, 75, 75, 76, 4861, 76, 75, 1, 70, 3042512690, 1, 74, 750163226, 66, 166, 167, 76, 935302372, 75, 70, 4882, 77, 74, 75, 74, 76, 2901165692, 76, 67, 76, 76, 74, 76, 75, 76, 75, 73, 74, 68, 76, 75, 70, 76, 76, 75, 70, 74, 76, 1, 71, 2751650064, 39, 6532, 1, 70, 688309200, 1, 71, 2723114381, 39, 6532, 0, 54, 168, 1305167000, 68, 75, 75, 55, 75, 75, 75, 19, 54, 75, 61, 76, 76, 55, 75, 76, 75, 75, 75, 19, 75, 75, 69, 75, 19, 75, 75, 75, 75, 56, 17, 76, 64, 56, 76, 77, 73, 75, 4964, 76, 75, 75, 75, 75, 75, 65, 73, 76, 0, 73, 77, 75, 76, 75, 4976, 75, 76, 62, 75, 75, 76, 73, 75, 73, 75, 76, 75, 75, 76, 1, 70, 4178460154, 1, 74, 250206546, 66, 169, 170, 76, 432562208, 76, 70, 5009, 77, 74, 75, 74, 76, 2163868563, 75, 70, 75, 76, 75, 73, 76, 75, 75, 75, 75, 74, 74, 68, 76, 75, 70, 76, 76, 76, 70, 74, 75, 1, 71, 3497214547, 39, 6532, 1, 70, 1721671780, 1, 71, 717846421, 39, 6532, 0, 57, 171, 3844995506, 68, 76, 75, 58, 76, 75, 75, 19, 57, 76, 61, 75, 75, 58, 75, 76, 76, 76, 75, 19, 75, 76, 69, 76, 19, 76, 76, 75, 75, 59, 17, 76, 64, 59, 75, 77, 73, 75, 5093, 75, 75, 75, 75, 76, 76, 65, 73, 75, 2, 73, 77, 76, 76, 76, 5105, 76, 75, 62, 75, 76, 76, 73, 76, 73, 76, 76, 75, 75, 76, 1, 70, 2135832442, 1, 74, 1204740770, 66, 172, 173, 76, 707595698, 76, 70, 5138, 77, 74, 75, 74, 75, 3864376183, 75, 70, 75, 75, 76, 73, 76, 76, 76, 76, 76, 74, 74, 68, 76, 75, 70, 76, 76, 75, 70, 74, 75, 1, 71, 3229584486, 39, 6532, 1, 70, 2949853391, 1, 71, 3958772903, 39, 6532, 0, 60, 174, 1288554436, 66, 175, 176, 75, 1854195989, 75, 60, 5192, 77, 61, 75, 19, 76, 3743194107, 75, 61, 76, 76, 61, 75, 76, 76, 76, 75, 19, 76, 76, 69, 75, 19, 75, 76, 76, 76, 62, 17, 76, 64, 62, 76, 77, 73, 75, 5227, 76, 75, 76, 76, 75, 75, 65, 73, 75, 1, 73, 77, 76, 76, 75, 5239, 75, 76, 1, 70, 1720921722, 1, 74, 1124817522, 66, 177, 178, 76, 1989770448, 76, 70, 5260, 77, 74, 75, 74, 76, 3035688179, 75, 67, 76, 76, 74, 76, 75, 75, 76, 73, 74, 68, 76, 75, 70, 76, 75, 75, 70, 74, 76, 1, 71, 2627927261, 39, 6532, 1, 70, 525963798, 1, 71, 2003581442, 39, 6532, 0, 63, 179, 1930510939, 68, 76, 76, 64, 75, 75, 76, 19, 63, 76, 61, 75, 76, 64, 75, 76, 75, 75, 76, 19, 76, 75, 69, 76, 19, 75, 75, 76, 76, 65, 17, 75, 64, 65, 76, 77, 73, 75, 5342, 75, 75, 75, 76, 75, 75, 65, 73, 75, 1, 73, 77, 76, 75, 75, 5354, 76, 75, 1, 70, 3064239815, 1, 74, 3660344015, 66, 180, 181, 76, 3085815619, 75, 70, 5375, 77, 74, 75, 74, 76, 85965788, 76, 67, 76, 76, 73, 76, 76, 75, 76, 74, 74, 68, 76, 76, 70, 76, 75, 76, 70, 74, 76, 1, 71, 475941923, 39, 6532, 1, 70, 644712874, 1, 71, 631167750, 39, 6532, 0, 66, 182, 4262763375, 68, 75, 76, 67, 75, 75, 76, 19, 66, 75, 61, 75, 75, 67, 76, 76, 75, 75, 76, 19, 76, 75, 69, 76, 19, 75, 76, 75, 75, 68, 17, 75, 64, 68, 76, 77, 73, 76, 5457, 75, 75, 76, 76, 75, 76, 65, 73, 76, 2, 73, 77, 76, 75, 76, 5469, 75, 75, 62, 75, 75, 75, 73, 75, 73, 75, 75, 75, 76, 76, 1, 70, 1286336465, 1, 74, 690232265, 66, 183, 184, 75, 3618455959, 76, 70, 5502, 77, 74, 76, 74, 75, 3539957385, 76, 70, 75, 75, 76, 74, 75, 76, 75, 75, 75, 73, 74, 68, 75, 75, 70, 76, 76, 76, 70, 74, 75, 1, 71, 2644762030, 39, 6532, 1, 70, 1791413342, 1, 71, 344455488, 39, 6532, 1, 70, 2107321783, 1, 71, 549389442, 39, 6532, 1, 69, 46078, 71, 75, 75, 2879706383, 75, 69, 76, 76, 75, 75, 76, 69, 76, 185, 17, 64, 69, 76, 77, 73, 75, 5576, 76, 75, 76, 75, 76, 76, 65, 73, 76, 2, 73, 77, 76, 75, 76, 5588, 75, 76, 1, 70, 1068239851, 1, 74, 3049532052, 66, 186, 187, 75, 1204423322, 76, 70, 5609, 77, 74, 76, 74, 76, 155589326, 76, 67, 76, 76, 74, 76, 75, 76, 76, 73, 74, 68, 76, 76, 70, 76, 76, 76, 70, 74, 76, 1, 71, 1605828686, 39, 6532, 1, 69, 36108, 68, 76, 76, 17, 76, 75, 76, 17, 69, 76, 1, 70, 3835091313, 1, 71, 4163615355, 39, 6532, 1, 69, 956, 66, 188, 189, 76, 2536397747, 75, 69, 5673, 77, 17, 76, 17, 76, 1254294653, 75, 1, 70, 2359405150, 1, 71, 3152532376, 39, 6532, 2, 7, 190, 4140056475, 0, 79, 191, 453850820, 8, 8, 7, 79, 1, 69, 19726, 68, 76, 75, 17, 76, 75, 75, 17, 69, 76, 1, 70, 2044587601, 1, 71, 1775867753, 39, 6532, 0, 79, 192, 1787359117, 8, 11, 3, 79, 0, 13, 4, 1007922522, 1, 69, 477, 66, 193, 194, 76, 3071462875, 76, 69, 5744, 77, 17, 75, 17, 76, 3556674366, 76, 1, 70, 3197057515, 1, 71, 3696704205, 39, 6532, 0, 79, 6, 0, 45, 79, 68, 75, 75, 79, 76, 76, 76, 4, 9, 75, 61, 75, 76, 79, 75, 76, 76, 75, 75, 4, 76, 75, 1, 69, 15380, 68, 76, 76, 17, 75, 75, 75, 17, 69, 76, 1, 70, 2238376401, 1, 71, 1343085462, 39, 6532, 61, 76, 76, 6, 76, 76, 76, 75, 75, 3, 76, 75, 0, 79, 195, 2407007900, 61, 76, 76, 79, 76, 76, 75, 76, 75, 4, 76, 76, 1, 69, 25501, 68, 75, 76, 17, 76, 75, 75, 17, 69, 76, 1, 70, 3510401268, 1, 71, 2115398218, 39, 6532, 0, 79, 7, 1967554067, 61, 76, 76, 79, 76, 76, 76, 75, 76, 5, 76, 75, 1, 69, 10963, 66, 196, 197, 75, 1466196550, 76, 69, 5884, 77, 17, 76, 17, 75, 2275470646, 76, 1, 70, 2400712595, 1, 71, 2864776019, 39, 6532, 72, 75, 75, 75, 198, 2355160843, 12, 79, 14, 75, 73, 10, 75, 80, 34051, 75, 75, 79, 78, 5916, 75, 75, 75, 75, 43, 9, 7, 8, 1, 80, 1, 69, 2114, 68, 76, 76, 17, 75, 75, 75, 17, 69, 75, 1, 70, 2300435433, 1, 71, 3437643213, 39, 6532, 45, 4, 0, 79, 199, 44868366, 8, 80, 3, 79, 71, 76, 76, 3823089174, 75, 79, 76, 76, 76, 76, 75, 80, 75, 200, 5, 64, 79, 76, 77, 73, 76, 5981, 76, 75, 76, 76, 76, 76, 65, 73, 75, 2, 73, 77, 76, 75, 76, 5993, 75, 76, 1, 70, 2776167258, 1, 74, 1671865960, 66, 201, 202, 76, 2225863572, 75, 70, 6014, 77, 74, 76, 74, 75, 2056327123, 76, 67, 76, 76, 73, 76, 75, 76, 75, 74, 74, 68, 75, 75, 70, 75, 75, 76, 70, 74, 75, 1, 71, 1609274569, 39, 6532, 1, 69, 23594, 66, 203, 204, 76, 3979463626, 76, 69, 6057, 77, 17, 76, 17, 75, 174519122, 76, 1, 70, 1820582477, 1, 71, 1630042388, 39, 6532, 1, 69, 5324, 68, 76, 75, 17, 76, 75, 75, 17, 69, 75, 1, 70, 2725254058, 1, 71, 1998874161, 39, 6532, 4, 2, 2, 79, 205, 2928564428, 42, 6, 79, 1, 0, 1, 69, 17202, 68, 76, 76, 17, 76, 76, 76, 17, 69, 76, 1, 70, 827273455, 1, 71, 1691013106, 39, 6532, 1, 69, 41083, 71, 76, 75, 323989858, 75, 69, 76, 76, 76, 76, 75, 69, 75, 206, 17, 64, 69, 75, 77, 73, 75, 6149, 75, 75, 75, 75, 76, 75, 65, 73, 75, 2, 73, 77, 76, 76, 76, 6161, 75, 76, 1, 70, 4283808181, 1, 74, 2898620180, 66, 207, 208, 75, 1785888778, 75, 70, 6182, 77, 74, 75, 74, 75, 2838589295, 76, 67, 75, 75, 74, 75, 75, 75, 75, 73, 74, 68, 76, 76, 70, 76, 75, 76, 70, 74, 76, 1, 71, 337447156, 39, 6532, 1, 69, 43654, 68, 76, 76, 17, 76, 75, 76, 17, 69, 76, 1, 70, 3212219132, 1, 71, 971351114, 39, 6532, 1, 69, 9932, 68, 76, 75, 17, 75, 75, 75, 17, 69, 75, 1, 70, 2303210295, 1, 71, 1552564489, 39, 6532, 1, 69, 37396, 71, 75, 76, 2422253667, 76, 69, 76, 75, 75, 75, 75, 69, 76, 209, 17, 64, 69, 75, 77, 73, 76, 6280, 75, 76, 75, 75, 76, 75, 65, 73, 75, 2, 73, 77, 76, 76, 76, 6292, 75, 76, 62, 76, 75, 75, 73, 76, 73, 76, 75, 76, 76, 75, 1, 70, 1466032700, 1, 74, 852763103, 66, 210, 211, 75, 1534643845, 75, 70, 6325, 77, 74, 75, 74, 76, 532031023, 76, 70, 76, 75, 75, 74, 75, 75, 76, 76, 76, 73, 74, 68, 76, 76, 70, 76, 76, 75, 70, 74, 76, 1, 71, 4180990295, 39, 6532, 1, 69, 0, 68, 75, 76, 17, 75, 75, 76, 17, 69, 75, 1, 70, 3727955511, 1, 71, 1988541448, 39, 6532, 1, 69, 8981, 66, 212, 213, 76, 15182356, 76, 69, 6391, 77, 17, 76, 17, 76, 4203661905, 76, 1, 70, 1605884843, 1, 71, 3154475145, 39, 6532, 0, 79, 214, 981596002, 68, 76, 76, 80, 76, 76, 75, 5, 79, 76, 61, 76, 76, 80, 76, 76, 75, 76, 75, 5, 76, 75, 1, 69, 11384, 66, 215, 216, 76, 1549833505, 76, 69, 6443, 77, 17, 76, 17, 75, 477083689, 75, 1, 70, 3241751836, 1, 71, 1300231072, 39, 6532, 68, 75, 76, 79, 75, 76, 76, 5, 13, 76, 43, 12, 3, 11, 1, 79, 0, 14, 217, 4178883632, 1, 69, 16881, 68, 76, 75, 17, 75, 75, 75, 17, 69, 75, 1, 70, 2809748118, 1, 71, 534695716, 39, 6532, 0, 79, 192, 1787359117, 8, 80, 3, 79, 43, 10, 3, 80, 1, 5, 1, 69, 47564, 66, 218, 219, 76, 1098928057, 75, 69, 6524, 77, 17, 75, 17, 75, 4169144530, 76, 1, 70, 3871272562, 1, 71, 885031361, 39, 6532, 42, 70, 72, 2, 71, 70, 58, 70, 0, 94, 220, 2618742161, 0, 93, 221, 4108437153, 0, 96, 7, 1967554067, 0, 95, 222, 3937788146, 47, 89, 11012, 2, 13, 0, 0, 4, 20, 0, 21, 223, 3972605183, 61, 94, 94, 21, 93, 94, 94, 94, 93, 22, 94, 94, 0, 23, 7, 1967554067, 61, 94, 94, 23, 94, 94, 94, 93, 93, 24, 94, 93, 0, 25, 224, 2574226500, 63, 93, 26, 22, 93, 93, 93, 25, 94, 93, 64, 26, 93, 95, 91, 94, 6624, 93, 93, 93, 93, 94, 94, 65, 91, 93, 0, 91, 95, 94, 94, 94, 6636, 94, 94, 62, 93, 94, 94, 91, 94, 91, 93, 94, 93, 94, 94, 1, 87, 3495530051, 1, 92, 2309925681, 66, 225, 226, 93, 1869638471, 93, 87, 6669, 95, 92, 93, 92, 94, 3863237525, 93, 70, 94, 94, 93, 92, 94, 93, 93, 94, 93, 91, 92, 68, 93, 93, 87, 94, 94, 94, 87, 92, 93, 1, 88, 61340456, 39, 10329, 0, 27, 227, 374721661, 61, 94, 94, 27, 94, 94, 94, 94, 94, 24, 94, 94, 69, 93, 24, 93, 93, 94, 94, 28, 22, 93, 64, 28, 94, 95, 91, 94, 6735, 93, 93, 94, 94, 94, 94, 65, 91, 93, 0, 91, 95, 93, 94, 93, 6747, 94, 93, 1, 87, 2343392611, 1, 92, 703385429, 66, 228, 229, 94, 81602731, 93, 87, 6768, 95, 92, 94, 92, 93, 3683411207, 94, 67, 94, 94, 92, 94, 93, 94, 93, 91, 92, 68, 94, 94, 87, 93, 94, 94, 87, 92, 93, 1, 88, 655762066, 39, 10329, 1, 87, 2852253135, 1, 88, 615402686, 39, 10329, 0, 29, 230, 3354292457, 66, 231, 232, 93, 482204485, 94, 29, 6820, 95, 30, 93, 24, 94, 3911623456, 93, 61, 94, 93, 30, 94, 94, 93, 94, 94, 24, 94, 93, 69, 94, 24, 94, 94, 94, 93, 31, 22, 94, 64, 31, 94, 95, 91, 94, 6855, 93, 94, 94, 94, 93, 94, 65, 91, 94, 1, 91, 95, 93, 93, 94, 6867, 93, 93, 1, 87, 624248520, 1, 92, 3279209126, 66, 233, 234, 94, 2730367606, 94, 87, 6888, 95, 92, 94, 92, 93, 2641570417, 94, 67, 93, 93, 92, 93, 93, 94, 94, 91, 92, 68, 93, 93, 87, 93, 93, 94, 87, 92, 93, 1, 88, 3544624333, 39, 10329, 1, 87, 907837827, 1, 88, 4017914065, 39, 10329, 0, 32, 235, 480313779, 68, 94, 94, 33, 93, 94, 93, 24, 32, 93, 61, 93, 93, 33, 93, 93, 93, 93, 93, 24, 93, 93, 69, 94, 24, 94, 93, 93, 93, 34, 22, 93, 64, 34, 93, 95, 91, 94, 6970, 94, 94, 94, 94, 94, 93, 65, 91, 94, 1, 91, 95, 93, 94, 94, 6982, 94, 93, 62, 93, 93, 93, 91, 93, 91, 93, 94, 94, 93, 94, 1, 87, 2634285886, 1, 92, 994280811, 66, 236, 237, 93, 256879534, 94, 87, 7015, 95, 92, 93, 92, 93, 1187926012, 94, 70, 94, 93, 94, 91, 94, 94, 93, 94, 94, 92, 92, 68, 93, 94, 87, 93, 94, 94, 87, 92, 94, 1, 88, 3006783534, 39, 10329, 1, 87, 1553028218, 1, 88, 4253012342, 39, 10329, 0, 35, 238, 1314022140, 68, 94, 93, 36, 94, 94, 93, 24, 35, 94, 61, 93, 93, 36, 94, 93, 93, 93, 94, 24, 94, 93, 69, 93, 24, 93, 94, 93, 93, 37, 22, 94, 64, 37, 93, 95, 91, 94, 7099, 94, 94, 94, 93, 94, 93, 65, 91, 94, 0, 91, 95, 93, 94, 93, 7111, 93, 93, 62, 94, 94, 93, 91, 93, 91, 94, 94, 94, 93, 93, 1, 87, 2116216305, 1, 92, 476209727, 66, 239, 240, 94, 111997416, 94, 87, 7144, 95, 92, 94, 92, 94, 3164463950, 93, 70, 93, 93, 94, 92, 93, 93, 94, 93, 94, 91, 92, 68, 93, 93, 87, 93, 94, 93, 87, 92, 94, 1, 88, 702084441, 39, 10329, 1, 87, 2231066999, 1, 88, 3045454343, 39, 10329, 0, 38, 241, 322352554, 68, 94, 94, 39, 94, 94, 94, 24, 38, 94, 61, 94, 93, 39, 94, 93, 94, 93, 93, 24, 93, 93, 69, 94, 24, 93, 94, 94, 93, 40, 22, 93, 64, 40, 93, 95, 91, 94, 7228, 93, 94, 93, 94, 94, 93, 65, 91, 93, 1, 91, 95, 93, 93, 93, 7240, 94, 93, 1, 87, 1992561272, 1, 92, 352554498, 66, 242, 243, 93, 2457082047, 93, 87, 7261, 95, 92, 94, 92, 93, 2888266923, 93, 67, 93, 93, 91, 93, 94, 94, 93, 92, 92, 68, 93, 94, 87, 94, 93, 93, 87, 92, 94, 1, 88, 872975968, 39, 10329, 1, 87, 49618458, 1, 88, 2329749364, 39, 10329, 0, 41, 244, 1263462643, 66, 245, 246, 93, 4057839024, 94, 41, 7313, 95, 42, 94, 24, 93, 1680400538, 93, 61, 93, 93, 42, 93, 94, 93, 94, 94, 24, 93, 94, 69, 93, 24, 93, 93, 94, 93, 43, 22, 94, 64, 43, 94, 95, 91, 93, 7348, 93, 94, 93, 93, 93, 94, 65, 91, 94, 1, 91, 95, 94, 93, 94, 7360, 94, 93, 62, 93, 93, 94, 91, 94, 91, 94, 93, 94, 93, 94, 1, 87, 3936530812, 1, 92, 2296526659, 66, 247, 248, 94, 2723455766, 94, 87, 7393, 95, 92, 94, 92, 94, 1473631318, 94, 70, 94, 93, 94, 91, 94, 94, 94, 93, 94, 92, 92, 68, 94, 93, 87, 94, 94, 94, 87, 92, 93, 1, 88, 1523681636, 39, 10329, 1, 87, 312735823, 1, 88, 2603198779, 39, 10329, 0, 44, 249, 4129734597, 66, 250, 251, 93, 28305571, 94, 44, 7447, 95, 45, 93, 24, 93, 2714325556, 93, 61, 93, 93, 45, 94, 93, 93, 94, 93, 24, 94, 93, 69, 93, 24, 94, 93, 93, 93, 46, 22, 93, 64, 46, 93, 95, 91, 93, 7482, 93, 93, 94, 93, 93, 93, 65, 91, 93, 2, 91, 95, 93, 94, 93, 7494, 94, 94, 62, 93, 94, 94, 91, 93, 91, 93, 94, 94, 93, 94, 1, 87, 1767164894, 1, 92, 127161676, 66, 252, 253, 93, 1967409213, 94, 87, 7527, 95, 92, 93, 92, 94, 407031761, 94, 70, 93, 94, 93, 92, 94, 93, 93, 94, 94, 91, 92, 68, 93, 93, 87, 94, 93, 94, 87, 92, 94, 1, 88, 1992758871, 39, 10329, 1, 87, 2948785852, 1, 88, 1588112382, 39, 10329, 0, 47, 254, 99347496, 68, 94, 94, 48, 94, 93, 94, 24, 47, 94, 61, 93, 93, 48, 93, 94, 93, 93, 94, 24, 94, 93, 69, 93, 24, 94, 93, 93, 93, 49, 22, 94, 64, 49, 93, 95, 91, 94, 7611, 93, 93, 93, 94, 94, 93, 65, 91, 94, 2, 91, 95, 94, 93, 93, 7623, 93, 94, 1, 87, 293487430, 1, 92, 2948449748, 66, 255, 256, 93, 1605504348, 93, 87, 7644, 95, 92, 94, 92, 93, 1928784902, 93, 67, 93, 93, 91, 94, 93, 93, 93, 92, 92, 68, 93, 93, 87, 93, 93, 93, 87, 92, 94, 1, 88, 3513675763, 39, 10329, 1, 87, 2379444569, 1, 88, 361632056, 39, 10329, 0, 50, 257, 197524532, 68, 94, 93, 51, 94, 93, 94, 24, 50, 94, 61, 94, 93, 51, 93, 93, 93, 94, 93, 24, 94, 93, 69, 94, 24, 94, 94, 94, 93, 52, 22, 94, 64, 52, 94, 95, 91, 94, 7726, 93, 94, 94, 94, 94, 94, 65, 91, 94, 1, 91, 95, 93, 94, 93, 7738, 94, 94, 1, 87, 3671368060, 1, 92, 2031363918, 66, 258, 259, 93, 702982081, 94, 87, 7759, 95, 92, 93, 92, 93, 1399581583, 94, 67, 93, 94, 92, 94, 93, 94, 93, 91, 92, 68, 94, 93, 87, 93, 93, 93, 87, 92, 94, 1, 88, 183034091, 39, 10329, 1, 87, 2768235231, 1, 88, 402935743, 39, 10329, 0, 53, 260, 640381854, 68, 94, 93, 54, 94, 93, 93, 24, 53, 93, 61, 94, 93, 54, 93, 93, 93, 93, 94, 24, 94, 93, 69, 94, 24, 93, 94, 94, 93, 55, 22, 94, 64, 55, 94, 95, 91, 93, 7841, 94, 93, 94, 94, 93, 94, 65, 91, 93, 2, 91, 95, 94, 94, 94, 7853, 94, 94, 62, 93, 94, 94, 91, 94, 91, 94, 94, 94, 94, 94, 1, 87, 3870682433, 1, 92, 2230674280, 66, 261, 262, 94, 3081146655, 93, 87, 7886, 95, 92, 94, 92, 93, 1557221786, 93, 70, 93, 93, 93, 92, 93, 93, 93, 94, 94, 91, 92, 68, 94, 94, 87, 93, 93, 93, 87, 92, 93, 1, 88, 1455919479, 39, 10329, 1, 87, 1302236119, 1, 88, 1904185647, 39, 10329, 0, 56, 263, 3724610801, 66, 264, 265, 94, 1784747971, 93, 56, 7940, 95, 57, 94, 24, 94, 2689436563, 94, 61, 93, 94, 57, 93, 94, 94, 93, 94, 24, 94, 94, 69, 93, 24, 93, 93, 94, 94, 58, 22, 94, 64, 58, 93, 95, 91, 94, 7975, 94, 94, 94, 93, 93, 93, 65, 91, 94, 2, 91, 95, 94, 94, 93, 7987, 94, 93, 1, 87, 2620527928, 1, 92, 980524903, 66, 266, 267, 93, 1318216338, 93, 87, 8008, 95, 92, 94, 92, 94, 467933999, 93, 67, 93, 93, 91, 93, 94, 93, 94, 92, 92, 68, 94, 94, 87, 93, 93, 94, 87, 92, 93, 1, 88, 3875789552, 39, 10329, 1, 87, 3598170482, 1, 88, 877388668, 39, 10329, 0, 59, 268, 1351125526, 66, 269, 270, 94, 104572596, 93, 59, 8060, 95, 60, 94, 24, 93, 565309750, 94, 61, 94, 94, 60, 94, 93, 94, 94, 93, 24, 94, 94, 69, 93, 24, 94, 94, 94, 93, 61, 22, 93, 64, 61, 94, 95, 91, 93, 8095, 94, 93, 94, 94, 93, 93, 65, 91, 94, 2, 91, 95, 94, 94, 94, 8107, 94, 94, 1, 87, 3583481803, 1, 92, 1943477725, 66, 271, 272, 94, 1870924256, 93, 87, 8128, 95, 92, 94, 92, 93, 950726711, 94, 67, 94, 93, 91, 93, 94, 94, 94, 92, 92, 68, 94, 94, 87, 93, 93, 94, 87, 92, 94, 1, 88, 4082555968, 39, 10329, 1, 87, 2166995442, 1, 88, 609325991, 39, 10329, 0, 62, 273, 3339937677, 68, 94, 94, 63, 94, 94, 94, 24, 62, 94, 61, 94, 94, 63, 94, 94, 94, 93, 94, 24, 94, 94, 69, 93, 24, 93, 94, 93, 94, 64, 22, 94, 64, 64, 94, 95, 91, 94, 8210, 93, 94, 94, 93, 93, 93, 65, 91, 94, 2, 91, 95, 94, 93, 93, 8222, 93, 93, 1, 87, 3778917513, 1, 92, 2138911655, 66, 274, 275, 93, 939327815, 94, 87, 8243, 95, 92, 93, 92, 93, 3059922163, 94, 67, 93, 94, 92, 94, 94, 93, 93, 91, 92, 68, 93, 93, 87, 94, 94, 94, 87, 92, 94, 1, 88, 3934359292, 39, 10329, 1, 87, 843729410, 1, 88, 1125635368, 39, 10329, 0, 65, 276, 1738594091, 68, 94, 93, 66, 93, 93, 93, 24, 65, 94, 61, 94, 93, 66, 93, 93, 94, 94, 94, 24, 93, 94, 69, 93, 24, 93, 94, 93, 93, 67, 22, 94, 64, 67, 93, 95, 91, 93, 8325, 94, 94, 94, 93, 93, 94, 65, 91, 93, 1, 91, 95, 94, 93, 94, 8337, 93, 93, 62, 93, 94, 94, 91, 93, 91, 94, 94, 93, 93, 94, 1, 87, 3632839813, 1, 92, 1992832171, 66, 277, 278, 94, 1288157511, 93, 87, 8370, 95, 92, 94, 92, 93, 799355481, 94, 70, 93, 94, 93, 91, 94, 94, 94, 93, 93, 92, 92, 68, 93, 93, 87, 94, 93, 93, 87, 92, 93, 1, 88, 1257667472, 39, 10329, 1, 87, 3476942212, 1, 88, 642904867, 39, 10329, 0, 68, 279, 1538703582, 66, 280, 281, 93, 39065183, 94, 68, 8424, 95, 69, 94, 24, 93, 2424054773, 93, 61, 94, 93, 69, 93, 93, 93, 94, 94, 24, 93, 93, 69, 94, 24, 94, 93, 93, 94, 70, 22, 94, 64, 70, 94, 95, 91, 94, 8459, 94, 94, 93, 94, 93, 93, 65, 91, 93, 1, 91, 95, 93, 94, 94, 8471, 94, 94, 1, 87, 3339868994, 1, 92, 1699862911, 66, 282, 283, 94, 235237261, 94, 87, 8492, 95, 92, 94, 92, 93, 4075724074, 94, 67, 93, 94, 91, 94, 93, 93, 93, 92, 92, 68, 93, 94, 87, 94, 94, 93, 87, 92, 93, 1, 88, 2720356662, 39, 10329, 1, 87, 2474780871, 1, 88, 1308394529, 39, 10329, 0, 71, 284, 1299929543, 68, 94, 94, 72, 94, 94, 94, 24, 71, 94, 61, 93, 93, 72, 93, 93, 93, 94, 93, 24, 93, 93, 69, 93, 24, 93, 94, 93, 94, 73, 22, 94, 64, 73, 94, 95, 91, 93, 8574, 94, 93, 93, 94, 94, 93, 65, 91, 94, 2, 91, 95, 93, 94, 93, 8586, 93, 94, 62, 93, 94, 94, 91, 94, 91, 94, 93, 93, 94, 94, 1, 87, 2983192325, 1, 92, 1343184359, 66, 285, 286, 94, 1897162609, 93, 87, 8619, 95, 92, 93, 92, 93, 4104619707, 94, 70, 94, 94, 93, 92, 93, 93, 94, 94, 94, 91, 92, 68, 93, 94, 87, 93, 94, 93, 87, 92, 93, 1, 88, 254265210, 39, 10329, 1, 87, 1440788567, 1, 88, 3472970858, 39, 10329, 0, 74, 287, 2780314180, 68, 93, 93, 75, 93, 93, 93, 24, 74, 94, 61, 93, 93, 75, 93, 93, 93, 93, 93, 24, 93, 93, 69, 93, 24, 93, 93, 94, 93, 76, 22, 93, 64, 76, 93, 95, 91, 93, 8703, 93, 93, 94, 93, 94, 93, 65, 91, 93, 1, 91, 95, 93, 93, 94, 8715, 94, 93, 62, 94, 93, 94, 91, 93, 91, 94, 94, 93, 93, 94, 1, 87, 4057975086, 1, 92, 2417975292, 66, 288, 289, 94, 3915954431, 94, 87, 8748, 95, 92, 94, 92, 94, 3760996563, 94, 70, 94, 94, 94, 92, 94, 93, 93, 94, 94, 91, 92, 68, 94, 94, 87, 94, 93, 93, 87, 92, 94, 1, 88, 651673274, 39, 10329, 1, 87, 3824741101, 1, 88, 3602261965, 39, 10329, 0, 77, 290, 566844328, 66, 291, 292, 94, 3856688181, 94, 77, 8802, 95, 78, 93, 24, 94, 2819708815, 94, 61, 94, 94, 78, 94, 94, 94, 93, 94, 24, 93, 94, 69, 94, 24, 93, 93, 93, 94, 79, 22, 93, 64, 79, 94, 95, 91, 93, 8837, 93, 93, 94, 94, 93, 94, 65, 91, 94, 0, 91, 95, 94, 93, 94, 8849, 94, 94, 62, 93, 93, 93, 91, 93, 91, 94, 94, 93, 94, 94, 1, 87, 3650452053, 1, 92, 2010450635, 66, 293, 294, 93, 2783888139, 94, 87, 8882, 95, 92, 94, 92, 94, 2894829281, 93, 70, 94, 93, 94, 92, 94, 94, 93, 94, 94, 91, 92, 68, 93, 94, 87, 94, 94, 93, 87, 92, 94, 1, 88, 3653873696, 39, 10329, 1, 87, 1032668252, 1, 88, 1338598644, 39, 10329, 0, 80, 295, 922524473, 66, 296, 297, 93, 738294823, 93, 80, 8936, 95, 81, 93, 24, 93, 1427740602, 93, 61, 93, 94, 81, 93, 93, 93, 93, 93, 24, 93, 93, 69, 93, 24, 93, 94, 93, 94, 82, 22, 94, 64, 82, 94, 95, 91, 94, 8971, 93, 94, 94, 94, 93, 93, 65, 91, 94, 1, 91, 95, 94, 94, 93, 8983, 93, 94, 1, 87, 4155385057, 1, 92, 2515383447, 66, 298, 299, 94, 4072459117, 94, 87, 9004, 95, 92, 94, 92, 94, 2136621332, 94, 67, 94, 94, 92, 94, 93, 94, 93, 91, 92, 68, 94, 94, 87, 94, 94, 94, 87, 92, 94, 1, 88, 23693883, 39, 10329, 1, 87, 3878296263, 1, 88, 3010034977, 39, 10329, 0, 83, 300, 3650481319, 68, 94, 94, 84, 94, 94, 94, 24, 83, 94, 61, 94, 94, 84, 94, 93, 94, 93, 93, 24, 94, 93, 69, 93, 24, 93, 93, 94, 94, 85, 22, 94, 64, 85, 94, 95, 91, 94, 9086, 93, 93, 93, 93, 93, 93, 65, 91, 93, 2, 91, 95, 93, 93, 93, 9098, 93, 93, 62, 93, 93, 93, 91, 93, 91, 93, 93, 93, 93, 93, 1, 87, 1717057529, 1, 92, 77050242, 66, 301, 302, 93, 1741377072, 93, 87, 9131, 95, 92, 94, 92, 93, 4031929530, 94, 70, 93, 93, 94, 91, 94, 94, 94, 93, 93, 92, 92, 68, 94, 93, 87, 93, 93, 94, 87, 92, 93, 1, 88, 3934219298, 39, 10329, 1, 87, 3779037857, 1, 88, 373952159, 39, 10329, 1, 87, 2558282657, 1, 88, 1966303593, 39, 10329, 0, 97, 191, 453850820, 8, 15, 14, 97, 0, 16, 192, 1787359117, 1, 86, 18984, 66, 303, 304, 94, 2695020909, 93, 86, 9204, 95, 22, 93, 22, 93, 2002007712, 94, 1, 87, 712354245, 1, 88, 3812233269, 39, 10329, 4, 5, 0, 97, 195, 2407007900, 61, 93, 94, 97, 94, 93, 93, 94, 93, 6, 94, 94, 1, 86, 14095, 66, 305, 306, 94, 2517286108, 93, 86, 9248, 95, 22, 93, 22, 93, 2014272294, 94, 1, 87, 637067210, 1, 88, 3887520230, 39, 10329, 0, 97, 6, 0, 45, 97, 73, 9, 93, 97, 55362, 94, 94, 10, 96, 9276, 94, 93, 93, 94, 61, 94, 93, 97, 94, 93, 93, 93, 93, 1, 94, 93, 0, 11, 307, 1419970025, 1, 86, 1315, 66, 308, 309, 93, 563166236, 94, 86, 9310, 95, 22, 93, 22, 93, 1662763162, 93, 1, 87, 2408318064, 1, 88, 2116270271, 39, 10329, 61, 93, 93, 18, 93, 93, 94, 93, 94, 6, 93, 94, 1, 86, 17385, 68, 94, 93, 22, 94, 93, 93, 22, 86, 94, 1, 87, 262324963, 1, 88, 4262262317, 39, 10329, 8, 97, 0, 16, 68, 94, 94, 98, 94, 94, 93, 2, 7, 94, 43, 17, 0, 97, 1, 98, 1, 86, 35818, 68, 94, 94, 22, 93, 93, 94, 22, 86, 94, 1, 87, 3836188177, 1, 88, 688400279, 39, 10329, 0, 97, 7, 1967554067, 61, 93, 94, 97, 93, 93, 93, 94, 93, 7, 94, 94, 1, 86, 31247, 68, 94, 93, 22, 94, 94, 94, 22, 86, 93, 1, 87, 351887122, 1, 88, 4172699926, 39, 10329, 70, 93, 94, 93, 13, 94, 93, 93, 93, 94, 12, 97, 61, 93, 93, 97, 93, 94, 93, 94, 94, 8, 93, 94, 2, 14, 190, 4140056475, 1, 86, 29131, 68, 93, 94, 22, 93, 94, 94, 22, 86, 94, 1, 87, 3716042985, 1, 88, 808542921, 39, 10329, 61, 94, 93, 19, 94, 94, 93, 93, 94, 7, 93, 94, 1, 86, 10204, 68, 93, 94, 22, 93, 93, 93, 22, 86, 94, 1, 87, 2056534291, 1, 88, 2468052484, 39, 10329, 1, 86, 38613, 71, 94, 94, 3619008408, 94, 86, 94, 93, 94, 94, 93, 86, 93, 310, 22, 64, 86, 93, 95, 91, 93, 9542, 94, 94, 94, 94, 94, 93, 65, 91, 94, 1, 91, 95, 93, 93, 94, 9554, 94, 94, 62, 94, 94, 94, 91, 94, 91, 94, 93, 94, 94, 94, 1, 87, 1305773923, 1, 92, 2340824863, 66, 311, 312, 94, 2615262126, 94, 87, 9587, 95, 92, 94, 92, 94, 2179158798, 94, 70, 94, 94, 94, 92, 93, 93, 94, 94, 94, 91, 92, 68, 94, 93, 87, 94, 94, 94, 87, 92, 94, 1, 88, 24443536, 39, 10329, 1, 86, 2583, 66, 313, 314, 94, 1204096478, 93, 86, 9632, 95, 22, 94, 22, 94, 3651361061, 94, 1, 87, 2865170818, 1, 88, 1659415543, 39, 10329, 1, 86, 11319, 68, 93, 93, 22, 93, 93, 94, 22, 86, 93, 1, 87, 1245497127, 1, 88, 3279090517, 39, 10329, 71, 94, 94, 2297770255, 93, 97, 93, 94, 94, 94, 94, 3, 94, 315, 7, 64, 97, 94, 95, 91, 93, 9689, 94, 94, 94, 93, 93, 93, 65, 91, 93, 2, 91, 95, 93, 93, 93, 9701, 94, 93, 1, 87, 1794097671, 1, 92, 3951631571, 66, 316, 317, 93, 3688725063, 94, 87, 9722, 95, 92, 93, 92, 93, 2720022108, 93, 67, 94, 93, 91, 94, 93, 94, 94, 92, 92, 68, 94, 94, 87, 94, 93, 93, 87, 92, 94, 1, 88, 3360799751, 39, 10329, 1, 86, 16780, 68, 94, 93, 22, 94, 93, 94, 22, 86, 94, 1, 87, 500148528, 1, 88, 4024438509, 39, 10329, 1, 86, 7312, 66, 318, 319, 94, 3680345934, 93, 86, 9786, 95, 22, 93, 22, 94, 123628625, 94, 1, 87, 1378349458, 1, 88, 3146237384, 39, 10329, 1, 86, 32824, 71, 94, 93, 3712219295, 93, 86, 94, 93, 93, 93, 94, 86, 94, 320, 22, 64, 86, 94, 95, 91, 93, 9825, 94, 93, 94, 94, 94, 94, 65, 91, 94, 2, 91, 95, 93, 93, 93, 9837, 94, 94, 1, 87, 3941920889, 1, 92, 682005536, 66, 321, 322, 94, 291335063, 93, 87, 9858, 95, 92, 93, 92, 94, 3603635835, 94, 67, 93, 94, 92, 93, 94, 93, 93, 91, 92, 68, 94, 94, 87, 93, 93, 94, 87, 92, 93, 1, 88, 225164487, 39, 10329, 1, 86, 39172, 66, 323, 324, 94, 3292807763, 93, 86, 9901, 95, 22, 94, 22, 94, 2776566149, 94, 1, 87, 1722310457, 1, 88, 2802277567, 39, 10329, 1, 86, 18801, 66, 325, 326, 94, 2120057787, 93, 86, 9927, 95, 22, 94, 22, 93, 3683817087, 94, 1, 87, 947823572, 1, 88, 3576764084, 39, 10329, 74, 94, 94, 94, 94, 8, 94, 94, 94, 17, 97, 94, 93, 94, 43, 98, 14, 15, 1, 97, 68, 93, 94, 18, 93, 94, 94, 6, 98, 94, 1, 86, 31220, 66, 327, 328, 93, 202696881, 93, 86, 9983, 95, 22, 93, 22, 94, 1119292869, 93, 1, 87, 3167369634, 1, 88, 1357216903, 39, 10329, 1, 86, 6197, 68, 94, 93, 22, 94, 93, 94, 22, 86, 94, 1, 87, 394644525, 1, 88, 4129942586, 39, 10329, 45, 6, 1, 86, 32942, 71, 94, 94, 4000592640, 94, 86, 93, 94, 94, 94, 94, 86, 93, 329, 22, 64, 86, 94, 95, 91, 93, 10045, 93, 94, 93, 94, 93, 93, 65, 91, 93, 0, 91, 95, 93, 93, 93, 10057, 94, 93, 1, 87, 3893416077, 1, 92, 633501485, 66, 330, 331, 93, 990739821, 93, 87, 10078, 95, 92, 93, 92, 93, 3798606104, 94, 67, 93, 94, 92, 93, 94, 93, 94, 91, 92, 68, 93, 93, 87, 93, 94, 93, 87, 92, 93, 1, 88, 2418154944, 39, 10329, 1, 86, 16826, 66, 332, 333, 94, 4248780910, 93, 86, 10121, 95, 22, 94, 22, 94, 1660962467, 94, 1, 87, 2091715289, 1, 88, 2432872335, 39, 10329, 1, 86, 3483, 66, 334, 335, 93, 1686953737, 94, 86, 10147, 95, 22, 94, 22, 93, 717028687, 94, 1, 87, 687264641, 1, 88, 3837322757, 39, 10329, 61, 93, 94, 7, 94, 93, 94, 93, 93, 97, 94, 93, 0, 97, 4, 1007922522, 68, 94, 94, 19, 93, 94, 93, 7, 97, 93, 1, 86, 17273, 66, 336, 337, 93, 1821412275, 94, 86, 10199, 95, 22, 93, 22, 94, 1269578127, 94, 1, 87, 3868911375, 1, 88, 655677584, 39, 10329, 0, 97, 338, 372864481, 68, 93, 94, 9, 93, 93, 93, 1, 97, 93, 0, 10, 7, 1967554067, 1, 86, 15032, 66, 339, 340, 94, 2020093095, 93, 86, 10243, 95, 22, 93, 22, 93, 2604632482, 94, 1, 87, 4054501067, 1, 88, 470087557, 39, 10329, 1, 86, 1424, 68, 94, 93, 22, 94, 93, 93, 22, 86, 94, 1, 87, 3281237358, 1, 88, 1243350361, 39, 10329, 75, 93, 93, 94, 11, 94, 94, 97, 1, 42011, 93, 93, 93, 74, 93, 94, 94, 94, 97, 93, 93, 93, 1, 12, 93, 93, 93, 0, 13, 341, 4257923869, 1, 86, 1402, 66, 342, 343, 93, 2332723540, 94, 86, 10321, 95, 22, 94, 22, 94, 1282402177, 94, 1, 87, 2295568533, 1, 88, 2229017592, 39, 10329, 42, 87, 89, 2, 88, 87, 0, 90, 344, 1849357816, 8, 87, 87, 90, 58, 87, 0, 4, 345, 3493829359, 0, 5, 346, 2411668063, 4, 3, 0, 6, 347, 3180401154, 68, 5, 5, 7, 5, 4, 5, 1, 6, 5, 0, 6, 7, 1967554067, 75, 5, 4, 5, 6, 5, 5, 8, 7, 27211, 5, 5, 4, 61, 5, 4, 8, 4, 5, 4, 5, 4, 1, 5, 5, 76, 4, 5, 1, 895694561, 5, 6, 5, 348, 5, 5, 4, 0, 7, 7, 1967554067, 75, 5, 4, 5, 7, 5, 5, 8, 6, 7771, 5, 5, 5, 61, 5, 4, 8, 5, 5, 4, 4, 4, 1, 5, 5, 0, 6, 349, 3918146136, 74, 5, 4, 5, 4, 6, 5, 4, 4, 1, 7, 5, 4, 5, 0, 6, 7, 1967554067, 75, 5, 5, 5, 6, 4, 5, 8, 7, 7337, 4, 5, 4, 61, 5, 5, 8, 5, 4, 4, 4, 4, 1, 5, 4, 74, 4, 4, 5, 4, 0, 4, 5, 5, 1, 6, 4, 5, 5, 0, 7, 7, 1967554067, 75, 5, 4, 4, 7, 5, 4, 8, 6, 28684, 4, 5, 5, 61, 5, 5, 8, 5, 5, 5, 4, 5, 1, 4, 5, 2, 6, 350, 961009787, 0, 7, 351, 3695879858, 8, 8, 6, 7, 0, 7, 352, 2133140262, 43, 9, 6, 8, 2, 1, 7, 0, 6, 7, 1967554067, 75, 4, 5, 5, 6, 5, 4, 7, 9, 65396, 5, 5, 4, 61, 5, 4, 7, 4, 5, 4, 5, 4, 1, 4, 4, 0, 6, 353, 2466473059, 75, 5, 4, 5, 6, 4, 5, 7, 1, 52694, 5, 5, 4, 74, 5, 5, 4, 4, 7, 4, 4, 4, 1, 6, 5, 4, 4, 0, 7, 7, 1967554067, 75, 4, 5, 5, 7, 4, 4, 8, 6, 38259, 4, 4, 4, 61, 5, 5, 8, 4, 4, 5, 5, 5, 1, 4, 5, 45, 1, 0, 6, 6, 0, 45, 6, 0, 5, 354, 2760472963, 0, 6, 355, 3470835140, 0, 4, 356, 1904040060, 0, 7, 7, 1967554067, 4, 3, 2, 8, 350, 961009787, 0, 9, 351, 3695879858, 8, 10, 8, 9, 0, 9, 357, 1499052592, 43, 11, 8, 10, 2, 1, 9, 0, 8, 7, 1967554067, 75, 4, 5, 5, 8, 5, 5, 9, 11, 56490, 5, 4, 5, 61, 5, 5, 9, 4, 4, 5, 5, 5, 1, 5, 5, 0, 8, 358, 1102878001, 75, 5, 5, 5, 8, 5, 4, 9, 1, 11716, 5, 4, 4, 74, 5, 4, 4, 5, 9, 4, 5, 5, 1, 8, 5, 5, 4, 0, 9, 7, 1967554067, 75, 5, 4, 4, 9, 4, 5, 10, 8, 31342, 4, 5, 4, 61, 4, 5, 10, 4, 4, 5, 4, 5, 1, 4, 4, 0, 8, 359, 2798737196, 68, 5, 5, 9, 4, 5, 4, 1, 8, 4, 0, 8, 7, 1967554067, 75, 4, 5, 5, 8, 4, 4, 10, 9, 56092, 5, 4, 4, 61, 4, 5, 10, 4, 4, 5, 5, 4, 1, 4, 4, 74, 4, 4, 5, 5, 0, 4, 5, 5, 1, 8, 4, 4, 5, 0, 9, 7, 1967554067, 75, 5, 5, 5, 9, 5, 4, 10, 8, 20709, 5, 5, 4, 61, 4, 5, 10, 4, 5, 4, 5, 4, 1, 4, 5, 0, 8, 360, 266387180, 66, 361, 362, 5, 1681757333, 5, 8, 10884, 6, 9, 4, 1, 4, 3499625660, 5, 0, 8, 7, 1967554067, 75, 4, 4, 5, 8, 5, 5, 10, 9, 22202, 4, 4, 4, 61, 4, 4, 10, 4, 5, 5, 5, 4, 1, 5, 5, 0, 8, 363, 3898171613, 66, 364, 365, 4, 1033384772, 4, 8, 10932, 6, 9, 5, 1, 5, 679356219, 5, 0, 8, 7, 1967554067, 75, 5, 5, 5, 8, 5, 5, 10, 9, 11134, 5, 4, 5, 61, 5, 5, 10, 4, 4, 4, 4, 5, 1, 5, 5, 0, 8, 366, 536487149, 68, 5, 4, 9, 4, 5, 4, 1, 8, 5, 0, 8, 7, 1967554067, 75, 5, 5, 5, 8, 5, 5, 10, 9, 22718, 4, 5, 5, 61, 4, 5, 10, 5, 4, 5, 4, 5, 1, 5, 4, 45, 1, 0, 8, 6, 0, 45, 8, 0, 7, 367, 973128249, 0, 6, 368, 2077841327, 0, 8, 7, 1967554067, 0, 4, 369, 2821668964, 0, 5, 370, 3430645546, 4, 3, 0, 9, 371, 459228533, 75, 5, 5, 5, 9, 5, 5, 10, 1, 28091, 4, 5, 4, 74, 4, 6, 5, 4, 10, 4, 6, 6, 1, 9, 5, 6, 5, 0, 10, 7, 1967554067, 75, 6, 6, 6, 10, 6, 4, 11, 9, 26055, 5, 6, 4, 61, 5, 4, 11, 4, 6, 5, 6, 5, 1, 4, 5, 0, 9, 372, 843194523, 66, 373, 374, 4, 335639079, 4, 9, 11113, 7, 10, 6, 1, 5, 1512848169, 4, 0, 9, 7, 1967554067, 75, 6, 6, 5, 9, 6, 4, 11, 10, 15851, 5, 6, 6, 61, 4, 6, 11, 4, 6, 6, 6, 6, 1, 6, 6, 68, 4, 5, 9, 6, 6, 5, 1, 0, 6, 0, 10, 7, 1967554067, 75, 5, 6, 6, 10, 4, 6, 11, 9, 41789, 4, 5, 5, 61, 5, 5, 11, 5, 5, 4, 5, 4, 1, 6, 4, 0, 9, 375, 2733362639, 72, 5, 5, 5, 376, 2760868630, 1, 10, 9, 6, 0, 9, 377, 1171681487, 75, 5, 6, 5, 9, 4, 4, 11, 1, 55331, 4, 4, 4, 73, 10, 6, 9, 25945, 5, 6, 11, 8, 11226, 4, 5, 6, 5, 0, 10, 7, 1967554067, 75, 4, 4, 4, 10, 5, 6, 11, 9, 59269, 4, 4, 5, 61, 6, 6, 11, 5, 5, 4, 6, 4, 1, 5, 5, 0, 9, 378, 795497668, 68, 4, 4, 10, 6, 6, 5, 1, 9, 4, 0, 9, 7, 1967554067, 75, 6, 5, 6, 9, 5, 5, 11, 10, 26566, 5, 6, 6, 61, 5, 4, 11, 6, 6, 6, 4, 5, 1, 6, 6, 2, 9, 350, 961009787, 0, 10, 351, 3695879858, 8, 11, 9, 10, 0, 10, 379, 467237444, 43, 12, 9, 11, 2, 1, 10, 0, 9, 7, 1967554067, 75, 5, 5, 4, 9, 5, 4, 10, 12, 58511, 5, 6, 5, 61, 6, 5, 10, 5, 6, 5, 5, 6, 1, 4, 5, 0, 9, 344, 1849357816, 49, 10, 1, 9, 1, 45, 10, 0, 9, 6, 0, 45, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 19; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_7_0_10_2_4_5_6_3_9_1_8 (order: [7,0,10,2,4,5,6,3,9,1,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[2]]; + // LTE + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a <= regs[base + _operands[2]]; + // LTE + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a <= regs[base + _operands[5]]; + // DIV + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a / regs[base + _operands[8]]; + // MOVE + var dst = _operands[9]; + regs[base + dst] = regs[base + _operands[10]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_6_1_3_9_7_10_8_0_2_4_5 (order: [6,1,3,9,7,10,8,0,2,4,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[5]]; + // MUL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a * regs[base + _operands[2]]; + // USHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >>> regs[base + _operands[5]]; + // NEQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a !== regs[base + _operands[8]]; + // UNARY_NEG + var dst = _operands[9]; + regs[base + dst] = -regs[base + _operands[10]]; + break; + } + case 63: + { + // ANTI_NEQ_2_6_7_3_0_5_8_1_4 (order: [2,6,7,3,0,5,8,1,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[6]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // GT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a > regs[base + _operands[5]]; + // NEQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a !== regs[base + _operands[8]]; + break; + } + case 64: + { + // ANTI_UNARY_NOT_6_4_0_5_8_1_10_3_11_2_7_9 (order: [6,4,0,5,8,1,10,3,11,2,7,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[10], _unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[6], _unsortedOperands[8]]; + // JUMP_IF_FALSE + var src = _operands[0]; + var target = _operands[1]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // DIV + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a / regs[base + _operands[4]]; + // UNARY_NOT + var dst = _operands[5]; + regs[base + dst] = !regs[base + _operands[6]]; + // GT + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a > regs[base + _operands[9]]; + // TYPEOF + var dst = _operands[10]; + regs[base + dst] = typeof regs[base + _operands[11]]; + break; + } + case 65: + { + // ANTI_UNARY_POS_1_5_3_0_6_2_10_4_7_8_9 (order: [1,5,3,0,6,2,10,4,7,8,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[6]]; + // UNARY_POS + var dst = _operands[0]; + regs[base + dst] = +regs[base + _operands[1]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[2]; + var pairCount = _operands[3]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[4]]; + var val = regs[base + _operands[5]]; + o[key] = val; + } + regs[base + dst] = o; + // JUMP_IF_FALSE + var src = _operands[6]; + var target = _operands[7]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // SHL + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a << regs[base + _operands[10]]; + break; + } + case 66: + { + // ANTI_SUB_1_12_10_13_11_5_7_6_3_9_4_0_2_8 (order: [1,12,10,13,11,5,7,6,3,9,4,0,2,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[11], _unsortedOperands[0], _unsortedOperands[12], _unsortedOperands[8], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[13], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[3]]; + // LOAD_CONST + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + // SUB + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a - regs[base + _operands[5]]; + // JUMP_IF_FALSE + var src = _operands[6]; + var target = _operands[7]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // BOR + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a | regs[base + _operands[10]]; + // LOAD_CONST + var dst = _operands[11]; + regs[base + dst] = this._constant(_operands[12], _operands[13]); + break; + } + case 67: + { + // ANTI_MUL_4_1_8_5_2_3_0_7_6 (order: [4,1,8,5,2,3,0,7,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[2]]; + // SUB + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a - regs[base + _operands[2]]; + // BXOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a ^ regs[base + _operands[5]]; + // MUL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a * regs[base + _operands[8]]; + break; + } + case 68: + { + // ANTI_ADD_4_6_0_3_8_7_1_2_5 (order: [4,6,0,3,8,7,1,2,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[4]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // LT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a < regs[base + _operands[8]]; + break; + } + case 69: + { + // ANTI_EQ_6_2_7_8_4_5_0_1_3 (order: [6,2,7,8,4,5,0,1,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[3]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // LTE + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a <= regs[base + _operands[5]]; + // LTE + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a <= regs[base + _operands[8]]; + break; + } + case 70: + { + // ANTI_BAND_1_4_2_10_3_7_5_0_6_9_8 (order: [1,4,2,10,3,7,5,0,6,9,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[9], _unsortedOperands[3]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // UNARY_POS + var dst = _operands[3]; + regs[base + dst] = +regs[base + _operands[4]]; + // EQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a === regs[base + _operands[7]]; + // BAND + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a & regs[base + _operands[10]]; + break; + } + case 71: + { + // ANTI_LT_5_12_10_11_0_13_7_4_3_8_2_6_9_1 (order: [5,12,10,11,0,13,7,4,3,8,2,6,9,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[13], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[12], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[5]]; + // LT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; + // NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a !== regs[base + _operands[5]]; + // UNARY_POS + var dst = _operands[6]; + regs[base + dst] = +regs[base + _operands[7]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[8]; + var name = this._constant(_operands[9], _operands[10]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // ADD + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a + regs[base + _operands[13]]; + break; + } + case 72: + { + // ANTI_SHL_0_1_2_4_5_7_6_8_3 (order: [0,1,2,4,5,7,6,8,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[7]]; + // SHL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a << regs[base + _operands[2]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[3]; + var name = this._constant(_operands[4], _operands[5]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // SHL + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a << regs[base + _operands[8]]; + break; + } + case 73: + { + // ANTI_BOR_1_7_0_12_5_8_2_9_10_4_11_6_3 (order: [1,7,0,12,5,8,2,9,10,4,11,6,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[12], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[8], _unsortedOperands[10], _unsortedOperands[3]]; + // BOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a | regs[base + _operands[2]]; + // USHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >>> regs[base + _operands[5]]; + // EQ + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a === regs[base + _operands[8]]; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[9]; + var target = _operands[10]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + // LOAD_INT + var dst = _operands[11]; + regs[base + dst] = _operands[12]; + break; + } + case 74: + { + // ANTI_BXOR_2_5_12_1_9_3_6_0_8_7_4_11_10 (order: [2,5,12,1,9,3,6,0,8,7,4,11,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[4], _unsortedOperands[12], _unsortedOperands[11], _unsortedOperands[2]]; + // LT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; + // UNARY_POS + var dst = _operands[3]; + regs[base + dst] = +regs[base + _operands[4]]; + // MOVE + var dst = _operands[5]; + regs[base + dst] = regs[base + _operands[6]]; + // BXOR + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a ^ regs[base + _operands[9]]; + // GT + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a > regs[base + _operands[12]]; + break; + } + case 75: + { + // ANTI_USHR_11_0_1_7_4_10_5_6_9_2_8_3 (order: [11,0,1,7,4,10,5,6,9,2,8,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[0]]; + // USHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >>> regs[base + _operands[2]]; + // UNARY_POS + var dst = _operands[3]; + regs[base + dst] = +regs[base + _operands[4]]; + // USHR + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a >>> regs[base + _operands[7]]; + // LOAD_INT + var dst = _operands[8]; + regs[base + dst] = _operands[9]; + // UNARY_NOT + var dst = _operands[10]; + regs[base + dst] = !regs[base + _operands[11]]; + break; + } + case 76: + { + // ANTI_UNARY_BITNOT_10_6_4_2_7_3_8_1_0_5_9 (order: [10,6,4,2,7,3,8,1,0,5,9]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[9], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[0]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this._constant(_operands[1], _operands[2]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // UNARY_BITNOT + var dst = _operands[3]; + regs[base + dst] = ~regs[base + _operands[4]]; + // GT + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a > regs[base + _operands[7]]; + // SHL + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a << regs[base + _operands[10]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/qualification.json new file mode 100644 index 00000000..2f48eb72 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/qualification.json @@ -0,0 +1,76 @@ +{ + "id": "exceptions--stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentation--84", + "case": "exceptions", + "profile": "stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentation", + "seed": 84, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true, + "concealConstants": true, + "antiInstrumentation": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/encoded.js new file mode 100644 index 00000000..94855e5a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/encoded.js @@ -0,0 +1,1856 @@ +var CONSTANTS = [/* 0 */-399130845, /* 1 */-1139653814, /* 2 */"ku6olQ81", /* 3 */"p99yMG9I8cEESJbwPViYrz44lpB2COqB9AjDkN8/56OMaF/wjEkdwN643E/G28Jg6OjHEbKJ3QCzmO/zDXef4Oqp2zHv2OFA0/voV/AoAaGlKUmRmngUg7kHMbO/KPphKPmUcCfbrUc/S6/QHmnm4TqZbDNYt0wDco9tcYaZMiG2uCTXU0tM/yRJctFMOXOhZudpc2IPo4IOOUqxZhkh515rbd9LS1ZB6NkKcYz2FGOcr2EznpYIAfJ5aRE2q/Q/KquHVjH5vEEC2ZOyGi+A4+Rm3PLLGfaBkQo5n8rrE2fl2qGxwTnvwshO/vPZV9wi/r7NESOKpH6ti+0X/Lr3LtpZkvH2TjLCzkcWsorePWLm6QWe0wqLhxMa944BeoPhEC6TsmLWj0Nn/ktSbdZaPl6qaxa5e0kOxZpANkOuc2Jv5lVyRd5YQitGMNJmarvmMRomrks6UoZmCk5yRRYWoq0/LNKeVizCnU0/1pi6DS5ymoZ2MYqeHSWGsTIunrbjHWb30uhN40Xh2sBOkvsCZoQqG767Fb+C5X70k9EX6qKzLu1V3vW/Ti/bzTcyq6Pex2WX9cKe7wLGh9EThO4sZbDlIy3/ugnHp0sm//5q96VyPbqSAxfdg0MPm5IwNYatX11V9xFLct8lS1Q1i90MbarmUxN3j0wDaIVoDTd9deVzq04/DMuBRHv9Kk1N1UGjTS9ikxV3O42IHRy1pTRZn7XrBXf83AFNJkWd1HZv9eMyZ70sMb2/BTaF5nzv6/InybzFLPdVQMXDTCXTuDfnrMMc+3X99fCcqwzMt8Ab6+w1ZKPV40zYvOHH6FOF/Plk76ULPIqMJQS9ewYMmZQZJLit9Fwu9K5DBdyptB80rN1FbJL0MxRqjFUEX5RE7Ep8K+QSVLY/OcRVRHv8Wmw4JEWkdCxPlHkEbIxKHDi0pyQvXGj0fnR23ElMVbRg1AhsrOMSlLQMFLzkBT2U5HxG7CskgrwWzLxFJ8SVTDPc4icrrJ8c2HXoBZKc8AzVtyIch+wwdPLVy0zMPOTXzlPr/NNkq5XrPcSMOwe4e/Ts06TNJfOtwVzExJRDE9+ZtAUllZ0BbZ30HBulj9w0BpWF7Wl9u9Q5U8c/BsO8VGb9bG1WJVW7CA9/m6AEAI2UHSOlTCM2X2jzNUdk3UpNPLVfJT1vkPs5l0INZb1HFWmVXX9C43dXirIJzblFUvWmbjHb6iciq/EehoUvBaaeATOrpx0jvu5ldZHF506eOs/XxGvU/s9q0ZU/PYaCJhfCc6zu5KKUNenN9FrNxrJL6d/KqgUm3J3IcuqG3gPlj+4y5Kbs7Vh6xdY7Sry/OcqEVj/6+W2TFimyjw9+gqV2J/KEHWSm8ipvXmTKekZB2hMuUqaLIhxukPMmhmYCUL5TFkjqYn5x6jRWM7odzpBZPPJ8bimiWDZ0slceZIJBCoyeHyqGpzVahM4aer7BMk4UMoWncmKf/n4Sh4k1PqiKZRase9nuylqNMcfJ+UIy9ptzDd6fovkpzJHjCumG9RvYr7A6raba6Q1xtsYiQ8K/3NLEZsnh7WnmFj+7pA8Qm812AumaIQ+p8DIGX23DhXY10espFqHEGiSPpPsdh+QKHLFrKVvhSX990wtHx8IJyclRb/krbyabazdYi1YBd4lW8X+ZBiOzt0JTXslFQV/ZclFjOxCnBWuG8BoJt4EoOZCQbeerYzDvN1HuOSXBj1hz955rQx+5qD0x7ZmVAMz41xPRr8ogGFm70V15xcD8S8O/+ijOYNf5spH6GOCA2w9NkIF4wuHUWZqw3sjqX9PL93Ax2I0hRbi7ECCIkgM6mIkQyLklIJDoL3D+y0xAjjgH0JtZGOBZaBKQaThsgEgYX3Ai6KRgQyh2sFRYRMBNeBTQY6hSIDGoWhCY+BYAvYl3MH2oaeBLeHLQdFiVwSLIhFA16J9jZhi9sDgIgIFxCDrwqRtjoKI4LFC1ySZAlsgdUPy/7SCQaMzx6omQAB+4tg8GmKdgrOnyUcDI8cDxX9Xz6mj94LUpRLGlGNOP//vikOQo+qH7KanQPX+WwwhHvTAwqYthB/m0cIqbfAe+iAERqGkd4aFYJTO7t0pDHclrcXPZZKFaO6WXXwucgEUJbZFoOTijVedxc2DPcEF+uTkxmFpJ51ZrSR8RiWsBTJlocW33j2Mnr6IyJVmmwWV57tZ7K4m/iyoIke35LYGsBgezgC8C47Zm0/HmWZPG8crIX93ruWYvmYgxybbTAvSPz/OElrIi0bnTUcbKNH6UyxtWxzraoewZnfLXDp6DxAetsgoeu2ES6qJeIMqjxz5ax86MeSbi7a4kMpGXHAK0/hUSr6lmPlWqUNYcek3ObFqOsTEux2JF5idyZh5wgmoGKJ5vamjmblo+rqc6CkZEznpyadZWIk2+ahI1lrD9DIrJFgC6oS8P2oN2PMpyQpfGK7KrXx3ilGYekrUNAoawGuGP5PvhZvoq070PVsbCEH6C06JW/SL9reoV8PrXDsOb0zfOqhkdtmUc9f9excP7x5VSju7eZTHls648O6mHHwvu/joVl6U9PQtTg9cJQ4HOeVWDtTst35pq91yLZx8PhVI1XJ1PbT3npFMZz2/FWEVBzWB9UudVK0m/VwtZhXn9OY2DFFKjbC9700N1V81BXWw1NKuAXxnrxnQEnegNPYWbFBBvLOP6ZD8tn70XRZrEE3+a6xKnyzzlrZIVzITMDO+TLjeqrEfduHXW9ORcwMv7N9JcwezYZeIVoIwEPLOH1Qz2/P9l9KXlPJJM", /* 4 */"8wTglNRv0/v/ZOms", /* 5 */-583948429, /* 6 */-1118819211, /* 7 */undefined, /* 8 */"idNaN3xNvd1vdQ==", /* 9 */420988975, /* 10 */-1432128360, /* 11 */1230631043, /* 12 */"EDfjqwEfp2o=", /* 13 */2093331420, /* 14 */-120933200, /* 15 */"RLFDWWLh", /* 16 */"F4S1DwG0", /* 17 */-744591188, /* 18 */"SfhYgFcIZ7A=", /* 19 */1084540407, /* 20 */"0K3ONeha6s6JezTXmyrjvfgV8pI=", /* 21 */-849567427, /* 22 */"1HHo6uQm9LvEL9Fa1cY=", /* 23 */"6mHW+PWQiw8Yg5MYBJHYeQ==", /* 24 */-946707493, /* 25 */"fVBDyGPAfVhy8A==", /* 26 */"uLQKL4PbPHewzCxd", /* 27 */-1362796680, /* 28 */"dfEQW72vODs=", /* 29 */"0Xz44+yXjDwwjYwF", /* 30 */1108441507, /* 31 */"jIZAGl6yUDYiorFvAvKIVg==", /* 32 */"SPMXbaSlHiW6rA==", /* 33 */-1628881952, /* 34 */"3NUzTsU6osc=", /* 35 */633728842, /* 36 */"2e7fYtbV6k39uu/G40vl/+Nq", /* 37 */"88nUUuYm1KPIH8HyKwm3gQ==", /* 38 */-1931628572, /* 39 */"v6pSNd/dkUKc9slThi8=", /* 40 */-10224837, /* 41 */"68PcV9zyP24=", /* 42 */-1511856467, /* 43 */"BXrNjxIahbYWKmliTeZwcv/f", /* 44 */"L2CR6ShQhziPz4tbn/DkaIQhPag=", /* 45 */-1066396310, /* 46 */"uewBp+EjGK8=", /* 47 */"4ur8nfs1+YLaHsnz63fq4g==", /* 48 */956246725, /* 49 */"oTEn2rpmBeqNHzCKhQY=", /* 50 */-2027686319, /* 51 */"2g70mdIx765P47dn", /* 52 */923954825, /* 53 */"+UPyzDi0sVVk7K18d5SYC3G8", /* 54 */388766280, /* 55 */"cG8k+6FgKqmgMSxJjMA2fw==", /* 56 */"d3+n4xqnqTI=", /* 57 */"2g5jgqgFII6zegnmlys2vg==", /* 58 */-581956793, /* 59 */"m087w6J4DfGHWTehpyg=", /* 60 */"dBhgsFMIeIA=", /* 61 */"Pw6kewEG", /* 62 */-38354472, /* 63 */"tWP16O8Qyrk=", /* 64 */55770711, /* 65 */"sp0VLbumEiOh7wZz", /* 66 */1851029020, /* 67 */"dtmwMSe568L5TyTzhFY9oQ==", /* 68 */"1Jn2Ae747xDwq8A/", /* 69 */"DeJpXkCyRjZ2Qg==", /* 70 */"opD5CNS/zAvekNX4sGE=", /* 71 */-1408534832, /* 72 */"RQ43gmZ2bvJ5HmyiTTYXXrHKAEY=", /* 73 */"26P9JvjSkE0axv5a", /* 74 */"DMlecVHpWyf8uw0vppo=", /* 75 */"fmNL531zft99RaRFcd1pa33neBs=", /* 76 */"h99RambmAZpRMg==", /* 77 */259981270, /* 78 */"X4VtDY+6Hic=", /* 79 */"nbxZB1SDWH9JlVEl", /* 80 */"puIwZ2mTYw8=", /* 81 */"C3G9CUGBhT4sk6QXL+PEbg==", /* 82 */"syQcbKb7EVSazI89IlSz1DV8tuM=", /* 83 */125689758, /* 84 */"n1yQo2g0", /* 85 */"i8wWR5w788z8VOLl", /* 86 */"aLS1DDichHU/9A==", /* 87 */329590022, /* 88 */"cUWU3gpr5+dQG0+tTDVNhbUON3M=", /* 89 */"mfY3YoUtEbaVWjLOsHs=", /* 90 */"siC4uLRTtM+sQA==", /* 91 */"so08PZ2U", /* 92 */-739459087, /* 93 */"7i+jpCoUmY0=", /* 94 */"E9qkdx3qp55qKkWy", /* 95 */"vgMBhrMyL56GZSDqsl4PyrbHj1o=", /* 96 */1262404268, /* 97 */"hEwL/IVXHKy0PBmUlRVHfAYMl5c=", /* 98 */-1254494314, /* 99 */"JkDb2fIw0qiNHw==", /* 100 */"XGh7IE6oKTBzmE8A", /* 101 */-339097089, /* 102 */"xquPYB75oREbma4A", /* 103 */"ecRjRDfMyzx4pA==", /* 104 */"pU7X0oN5", /* 105 */"MRNvuVUBW5l88X9rRd9cqw==", /* 106 */-2092464422, /* 107 */"J3uR0CxZgsEOyYtQ//cjYw==", /* 108 */"gbE7CYiAGAei8w==", /* 109 */-88743896, /* 110 */"CcW2TQv9p2YwI7K/VBPshg==", /* 111 */"IPK/djHi5q3KNsjalk8=", /* 112 */1439006934, /* 113 */"colIMVSpS2MP/2xTdNchQZpJLdE=", /* 114 */"nBeJfCfsh1U=", /* 115 */"S+q9Hj6q", /* 116 */"U8tBf3XFU1VqvWwtImc=", /* 117 */"w6jJMM1Zz8GCeP/Q", /* 118 */"PUKe1lSyXSp/5g==", /* 119 */"TgR8lGtsfPw=", /* 120 */"SiCR2WVw", /* 121 */"5jP7r+Tg7ng=", /* 122 */"xSzftMzb3k/N9DVVsq0=", /* 123 */"Q9KGZ2vi/h5zsg==", /* 124 */"rv8uaqUWJ7q+AUyWXnJt7mYi", /* 125 */"i6DxKJyzNx++gEEJ", /* 126 */true, /* 127 */"v9ZbPQtN", /* 128 */1005759568, /* 129 */"Ur5UKumWOPqDjUcGnbI3Lp7j", /* 130 */553089729, /* 131 */"/dzxTIxHMts=", /* 132 */2062747294, /* 133 */1328447795, /* 134 */-1926709875, /* 135 */371382246, /* 136 */-726127132, /* 137 */"OHL13u8jxre3Et6O7QGwmQ==", /* 138 */"bPZTakjeUiobtosaHo6XBTuSvi4=", /* 139 */"EeF8eT2XmwsLvw==", /* 140 */488447862, /* 141 */38237754, /* 142 */"ENiOYxTojpAMCKSB", /* 143 */"40rctuPd", /* 144 */-223300483, /* 145 */-2011957414, /* 146 */"WR4xiowGD5p/Mlu2IyI=", /* 147 */"waLG7sdzIgaygg8+pJECag==", /* 148 */-1505386512, /* 149 */"AIK2Nh2S+u8RYpzWGVLLPQ==", /* 150 */"/FEAybh+", /* 151 */"qmlh8UUXaYM=", /* 152 */-1707842749, /* 153 */"OtmqoSA5kZY=", /* 154 */38752923, /* 155 */"k1EI+bhAK8eHMxavkOA=", /* 156 */-1977967151, /* 157 */-1650277792, /* 158 */"NWa/6lAesYU=", /* 159 */458482408, /* 160 */"qUAw14SzUC+s4J55+fHTGA==", /* 161 */-321426038, /* 162 */"43zIzPdHqDs=", /* 163 */"E6yvZzb85Aw=", /* 164 */-487984793, /* 165 */"S0rE/idKbNJsvksiDeZxcg==", /* 166 */"FWBq+H4QvokTALGI", /* 167 */668609043, /* 168 */"rD0qRWLLgH8g65ImPL0=", /* 169 */"IlC860JgURh5sAw4Z4BFCA==", /* 170 */"+PX8BYyM7Dzxl/fr", /* 171 */1004691606, /* 172 */"92AA6I4TJriCABOYjfE=", /* 173 */591361329, /* 174 */"de+qowonRrGUGS2BpwgTn4or", /* 175 */"kLMxJ5uiCu3JdS/F", /* 176 */1504752500, /* 177 */"eRtMgXt5bpFSKQ==", /* 178 */"tf7VZeLVxz7KQp3HFHOC7g4ly6U=", /* 179 */487208041, /* 180 */"FMJuTmbSEDafog==", /* 181 */1780197288, /* 182 */"W/1QZS8r", /* 183 */"pjsTrpoaZIaECeySJTaWow==", /* 184 */-1792317209, /* 185 */"EC1KvUIXYosdf6iKThU=", /* 186 */"N3vY59uopTDEyflB", /* 187 */"1ELzxs1TQD6roiFm", /* 188 */1795754930, /* 189 */"dzIhjkQCwYYW8r+daSZ+qg==", /* 190 */-2122693895, /* 191 */"ZeaLmjcOk7sNBqeKIn6n6Qgil74=", /* 192 */-1328264119, /* 193 */"q1Io+WZBXdkWt5ArKp8=", /* 194 */"jg1ndvQa", /* 195 */"FRGheR0BjYY=", /* 196 */-1485751320, /* 197 */"rDeaqBfgm2k=", /* 198 */"jrFCWV3PAnsG1+mo", /* 199 */667338810, /* 200 */"07nFDuSb0+f/asre", /* 201 */"DLmK0BNrmf9qaxIRWrk=", /* 202 */"kFEw6ZZgNuuNlwo7cYECGQ==", /* 203 */"XN1bp6orDZ+RdDCF", /* 204 */83370703, /* 205 */"h6MCHoRyDfb7gZ4K", /* 206 */"XwdBgzcJoXEsGZ2gETut1xpLs8A=", /* 207 */"+dmkoN4oipM9d4SAARk=", /* 208 */"+MsMvIlEE9SWfTDs", /* 209 */"DaxkJHuUSAwJfMAHBow+PA==", /* 210 */487953519, /* 211 */"9MjrwxtHl9AbefLh", /* 212 */"0eCJe/nv", /* 213 */"WI6hEmem9CNt3lJyqck1XvzKD7Y=", /* 214 */"1JsibqP6eVZJzmHCREY=", /* 215 */"26Xa3clyDsejQw3PmzIdpdjt+XrVFtmL", /* 216 */"7ecNepXu7Vk=", /* 217 */"aDJmtkoiGW4=", /* 218 */"0rXYLM5csfe8cw==", /* 219 */"pncJ7J8cBaSbNSdcvcw=", /* 220 */"", /* 221 */"YLJ6Jktiaf5jUg==", /* 222 */"kDN6j6MEKHU=", /* 223 */"Wq1NJTKbnm8o+5oG", /* 224 */-2061347567, /* 225 */"E4KROReWgOr+f93r7FY=", /* 226 */1927405225, /* 227 */1541658279, /* 228 */"5uz4fMuV/RX3vPcM", /* 229 */"AX6q4lsW0b01ClSGTPo=", /* 230 */"Xl4BMli2VF4g6nt2MZo8Dk+6", /* 231 */-183362348, /* 232 */"3RGziAoD", /* 233 */"dHV/7REjyKchMy9dXMVdRQ==", /* 234 */"hCHoWdLuzHq+xyBK", /* 235 */722181916, /* 236 */"XfiJYBQo", /* 237 */1126029127, /* 238 */"g/ZlG+OvRjtJVVPNSX1bxQ==", /* 239 */"GGpm/kNKTMY=", /* 240 */1834472769, /* 241 */"cQqCkUz2RGK63w==", /* 242 */504624432, /* 243 */"h4ksAZyIDTM=", /* 244 */"2CeRuhMu1NU=", /* 245 */1487859756, /* 246 */"vCTVpbRtxfw=", /* 247 */"JdumTz/7", /* 248 */"TypoUkH+ZEJZxmsys64R4jp6Su4=", /* 249 */"17jiIN+X", /* 250 */924281567, /* 251 */-396309909, /* 252 */"Pecia1sfHIW4BT6dqnI=", /* 253 */1341859816, /* 254 */"6Mn4Qa/IFleXuw==", /* 255 */1058042256, /* 256 */"41zALful2xTWjPsD", /* 257 */28251767, /* 258 */-2056730499, /* 259 */"Pzp+VjzqRH5f6h6S", /* 260 */1025557281, /* 261 */"jjUHnebsdneb28ZMf0U=", /* 262 */1654771426, /* 263 */"+3brwtNZ0DGiqgJfgOu7duKa6QE=", /* 264 */"4iba2vFxyvnqQj7PhbMeVw==", /* 265 */-298274108, /* 266 */"KcTpVM0/iqs=", /* 267 */"Y95WanrinJ4rMoyHKQI=", /* 268 */-1027828077, /* 269 */"92nV4aNo2hCAv/sD", /* 270 */"lFYG+oRPLNqoNjaqCuKhZg==", /* 271 */"SKcKObmREhmEgBIP", /* 272 */197357027, /* 273 */"E4SkBLCPPRSvpDYsx10=", /* 274 */"C30EjX0HgLsaL4ea", /* 275 */"+gr4ntx13+XNXhKy", /* 276 */-1523022885, /* 277 */"vUgm0Ks5XKCuGGFw4gs=", /* 278 */"R8h5sGYo", /* 279 */"K6eHUgfJzEE=", /* 280 */-189303532, /* 281 */"BqVWbUP7", /* 282 */"iAFliOhwjuv3J8O4", /* 283 */"Y6wT5LRkVOw=", /* 284 */589180034, /* 285 */"DEkC8ZBYAiM=", /* 286 */"g2qA4fZpvdImP5RD", /* 287 */"pYMhD4WSDHma4XNZ", /* 288 */673855338, /* 289 */"mCn6sNfYhk80+71Qeam6ISipiBA=", /* 290 */"kWgjE7W3RQORmA1xg+lgYQ==", /* 291 */"pxgScYLpPGHaKKK3ZVu7wDd5", /* 292 */"0FzH6xdntBwEjaUFyITkDPPz22c=", /* 293 */-1886693973, /* 294 */-715207, /* 295 */"mTlgsEzYT2B06FGQaghkgA==", /* 296 */497703651, /* 297 */"ToN0P06VTBU=", /* 298 */-462288844, /* 299 */"ZJxk9G+EdhxrtCc0VZx/bAP0T1Q=", /* 300 */"gtMwd4zgMBmysesI9IA=", /* 301 */1842186536, /* 302 */"7Bvgt8Yq3p61aQ==", /* 303 */"aqFN71RzENcVQy25mdE=", /* 304 */"kIo0Hr6FBfKrnhwiqKcCUnbO", /* 305 */"cIyV+waUnSwpvKBV", /* 306 */"dDRRnL1tUuTSVAXMtDtpRA==", /* 307 */"rJw+9I11Ixw=", /* 308 */1030576684, /* 309 */"U7mCAEubnnce64kgA6k=", /* 310 */"9V6Dwu1J49HVKram", /* 311 */"uYPgPviS8hU=", /* 312 */-349356098, /* 313 */"fjVI3XRNJMVFW7QvPruGJiftR30=", /* 314 */-1193039935, /* 315 */"usEcSZHSJDeAow==", /* 316 */"NOqEZ8La1E4=", /* 317 */"PtWCTQu9esdCUx7/SGNqpeI9", /* 318 */"xDpsx79LWv8=", /* 319 */-2082088441, /* 320 */"TnMUD1iDfgd8jXsdXeVhZXHf", /* 321 */"1Xmc4DpbhbcOy5dAfcmEcWnZjyA=", /* 322 */"u54BMq+nGCL+bgLyrlk=", /* 323 */1216208894, /* 324 */"vvW0gsEfoLP9Nsmijm0=", /* 325 */"vA8Ue1wRWqliIVOp", /* 326 */14070421, /* 327 */1986696066, /* 328 */"klrCNuLJ", /* 329 */"K+G/aErzdF8Uww==", /* 330 */1112637340, /* 331 */-98196970, /* 332 */-1148796388, /* 333 */1853817271, /* 334 */"kOEzeIKQDBiSg8MIzJDd+QZg", /* 335 */"/ojOAwKY", /* 336 */"iz7Kqd0R0v4=", /* 337 */-1313699278, /* 338 */492512504, /* 339 */-1476813368, /* 340 */233521091, /* 341 */"wd0lSqs2F9oVbhT6HhY=", /* 342 */1828975839, /* 343 */"VjOPpwsj3W4f9a6FZw0=", /* 344 */"+B6Nq9I32VoyzQ==", /* 345 */1550098832, /* 346 */"GQ75mvEFy40=", /* 347 */"53Ps6MZgtakeMQ==", /* 348 */-572057489, /* 349 */-1248923538, /* 350 */1682414994]; +var BYTECODE = [82, 83, 84, 47, 6, 3456, 1, 84, 0, 0, 85, 42, 5, 6, 1, 7, 47, 8, 6537, 4, 93, 0, 0, 86, 47, 15, 111, 1, 97, 1, 0, 1, 5, 61, 2, 14, 12, 12, 14, 15, 12, 12, 87, 88, 89, 90, 42, 3, 8, 4, 5, 9, 10, 11, 0, 16, 5, 3711018866, 62, 16, 17, 13, 14, 12, 13, 12, 42, 16, 2, 1, 17, 0, 17, 6, 3176148081, 42, 18, 2, 1, 17, 91, 1, 9, 3976635791, 1, 10, 243, 1, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 92, 48, 17, 3, 16, 18, 19, 93, 0, 15, 7, 0, 94, 0, 92, 8, 3568421575, 0, 95, 9, 420988975, 0, 94, 10, 2862852820, 0, 93, 11, 1230632463, 0, 91, 12, 2776412445, 47, 88, 9738, 2, 13, 0, 0, 95, 0, 20, 13, 2093350740, 61, 21, 91, 91, 91, 91, 20, 91, 91, 0, 22, 9, 420988975, 61, 23, 93, 92, 91, 91, 22, 93, 91, 0, 24, 14, 4174038507, 63, 21, 24, 93, 91, 93, 93, 91, 25, 57938, 2956740842, 91, 91, 15, 93, 64, 25, 93, 16, 91, 92, 93, 93, 92, 93, 92, 3486481077, 93, 89, 65, 92, 91, 91, 17, 89, 91, 89, 92, 3550392789, 1, 86, 860857580, 1, 90, 2602120590, 66, 93, 18, 90, 90, 93, 86, 92, 91, 4061134968, 67, 93, 89, 91, 0, 90, 93, 91, 93, 90, 91, 92, 92, 92, 92, 68, 1, 92, 91, 93, 93, 93, 86, 91, 86, 90, 93, 0, 92, 91, 91, 93, 1, 87, 4234527423, 77, 0, 26, 19, 1084541897, 61, 23, 93, 93, 93, 91, 26, 93, 93, 69, 93, 67257012, 20, 21, 93, 91, 23, 27, 93, 64, 27, 91, 16, 91, 91, 93, 93, 93, 93, 93, 3486481077, 92, 89, 65, 92, 92, 91, 21, 89, 92, 89, 93, 3445363154, 62, 89, 89, 93, 93, 91, 93, 91, 1, 86, 575027732, 1, 90, 1480811845, 66, 93, 22, 90, 90, 92, 86, 91, 92, 2029932155, 70, 39934, 345, 92, 94, 90, 2761555317, 89, 92, 93, 92, 91, 90, 23, 68, 1, 92, 92, 93, 92, 92, 86, 91, 86, 90, 92, 1, 91, 93, 93, 92, 1, 87, 2985624265, 77, 1, 86, 1754149193, 1, 87, 787531581, 77, 0, 28, 24, 3348258193, 68, 0, 91, 93, 92, 92, 92, 23, 91, 29, 28, 92, 2, 93, 93, 93, 91, 61, 23, 92, 93, 92, 92, 29, 92, 93, 69, 92, 884690417, 25, 21, 92, 91, 23, 30, 93, 64, 30, 93, 26, 93, 93, 92, 92, 92, 92, 93, 3167482679, 92, 89, 65, 93, 92, 91, 27, 89, 93, 89, 91, 2932175114, 62, 89, 89, 93, 91, 91, 93, 91, 1, 86, 1891135731, 1, 90, 2393706983, 66, 91, 28, 90, 90, 93, 86, 93, 93, 3964283721, 70, 17150, 475, 93, 94, 89, 1679619160, 90, 92, 91, 92, 93, 90, 29, 68, 2, 92, 91, 93, 91, 93, 86, 92, 86, 90, 92, 0, 93, 92, 93, 93, 1, 87, 3400816546, 77, 1, 86, 83604881, 1, 87, 2896625107, 77, 0, 31, 30, 1108434904, 68, 2, 92, 93, 92, 92, 93, 23, 92, 32, 31, 92, 2, 93, 91, 92, 92, 61, 23, 91, 92, 92, 91, 32, 92, 91, 69, 93, 827490508, 31, 21, 92, 92, 23, 33, 91, 64, 33, 92, 32, 93, 91, 91, 92, 91, 92, 93, 1563209287, 91, 89, 65, 93, 91, 91, 33, 89, 91, 89, 91, 2666117758, 1, 86, 1725402723, 1, 90, 2363283503, 66, 93, 34, 90, 90, 91, 86, 92, 93, 495029895, 67, 93, 90, 91, 0, 90, 93, 92, 91, 89, 92, 92, 91, 93, 91, 68, 0, 92, 91, 93, 92, 92, 86, 91, 86, 90, 93, 2, 93, 91, 93, 91, 1, 87, 33098716, 77, 1, 86, 2774789825, 1, 87, 1439087773, 77, 0, 34, 35, 633719085, 68, 0, 91, 91, 93, 93, 93, 23, 91, 35, 34, 93, 2, 91, 93, 93, 93, 61, 23, 91, 93, 93, 91, 35, 93, 91, 69, 91, 3910425401, 36, 21, 91, 93, 23, 36, 92, 64, 36, 91, 37, 91, 93, 93, 93, 92, 91, 91, 3830473707, 91, 89, 65, 91, 91, 93, 38, 89, 93, 89, 93, 2363344884, 1, 86, 4095323623, 1, 90, 437764758, 66, 92, 39, 90, 90, 93, 86, 91, 93, 466485524, 67, 92, 90, 92, 2, 90, 91, 92, 93, 89, 93, 93, 93, 92, 93, 68, 1, 93, 93, 91, 92, 93, 86, 93, 86, 90, 91, 1, 93, 92, 92, 93, 1, 87, 2753682775, 77, 1, 86, 719062132, 1, 87, 4281225402, 77, 0, 37, 40, 4284723490, 68, 2, 92, 91, 93, 91, 93, 23, 92, 38, 37, 91, 1, 91, 92, 91, 91, 61, 23, 93, 92, 92, 92, 38, 91, 92, 69, 92, 3486481077, 16, 21, 93, 92, 23, 39, 93, 64, 39, 91, 41, 93, 93, 92, 93, 92, 92, 91, 3496426906, 91, 89, 65, 92, 92, 92, 42, 89, 91, 89, 91, 2783062811, 62, 89, 89, 91, 93, 93, 92, 92, 1, 86, 999796530, 1, 90, 1502499032, 66, 93, 43, 90, 90, 91, 86, 91, 92, 1790100350, 70, 45969, 851, 91, 94, 89, 3488913244, 90, 91, 93, 92, 92, 90, 44, 68, 2, 92, 92, 92, 93, 93, 86, 91, 86, 90, 92, 1, 92, 92, 93, 91, 1, 87, 3017039891, 77, 1, 86, 1140720431, 1, 87, 2213584002, 77, 0, 40, 45, 3228578158, 66, 92, 46, 41, 23, 92, 40, 92, 91, 1130587760, 61, 23, 92, 92, 92, 92, 41, 92, 91, 69, 93, 3910425401, 36, 21, 92, 92, 23, 42, 93, 64, 42, 92, 47, 91, 93, 93, 92, 92, 92, 92, 1117680260, 92, 89, 65, 91, 93, 92, 48, 89, 92, 89, 92, 956294884, 1, 86, 1509870901, 1, 90, 2012999364, 66, 92, 49, 90, 90, 92, 86, 92, 92, 4291708998, 67, 92, 89, 92, 2, 90, 92, 92, 92, 90, 93, 91, 91, 92, 93, 68, 1, 93, 93, 92, 92, 91, 86, 93, 86, 90, 91, 1, 93, 91, 91, 91, 1, 87, 2765416243, 77, 1, 86, 3068425757, 1, 87, 2442461948, 77, 0, 43, 50, 2267240213, 66, 91, 51, 44, 23, 91, 43, 91, 91, 2026519780, 61, 23, 93, 92, 93, 91, 44, 91, 91, 69, 91, 884690417, 25, 21, 91, 91, 23, 45, 92, 64, 45, 91, 37, 93, 91, 91, 93, 91, 91, 91, 3830473707, 92, 89, 65, 91, 91, 93, 52, 89, 93, 89, 93, 923941315, 1, 86, 710077303, 1, 90, 1347917794, 66, 93, 53, 90, 90, 93, 86, 93, 91, 1950373459, 67, 91, 90, 91, 2, 90, 93, 92, 93, 89, 92, 91, 93, 93, 92, 68, 2, 92, 92, 91, 91, 93, 86, 91, 86, 90, 93, 2, 91, 93, 92, 93, 1, 87, 3719225158, 77, 1, 86, 3332162271, 1, 87, 2618154910, 77, 0, 46, 54, 388796003, 68, 2, 92, 92, 93, 93, 91, 23, 92, 47, 46, 93, 2, 92, 93, 93, 92, 61, 23, 93, 91, 92, 92, 47, 93, 93, 69, 92, 1563209287, 32, 21, 92, 91, 23, 48, 92, 64, 48, 93, 55, 92, 92, 93, 92, 92, 92, 92, 1237309130, 92, 89, 65, 91, 93, 93, 56, 89, 91, 89, 91, 2223042850, 1, 86, 3078675994, 1, 90, 3715503072, 66, 93, 57, 90, 90, 93, 86, 93, 93, 1868622010, 67, 91, 90, 93, 2, 90, 92, 93, 91, 89, 91, 91, 91, 93, 91, 68, 2, 91, 93, 93, 93, 93, 86, 91, 86, 90, 91, 0, 91, 93, 92, 92, 1, 87, 2180300806, 77, 1, 86, 4256871259, 1, 87, 1092534505, 77, 0, 49, 58, 3713046615, 66, 93, 59, 50, 23, 92, 49, 92, 92, 22362619, 61, 23, 93, 92, 93, 93, 50, 93, 92, 69, 92, 827490508, 31, 21, 93, 92, 23, 51, 91, 64, 51, 91, 16, 93, 93, 93, 92, 93, 93, 91, 3486481077, 92, 89, 65, 93, 92, 92, 60, 89, 92, 89, 92, 552494982, 1, 86, 948375311, 1, 90, 1585729712, 66, 93, 61, 90, 90, 93, 86, 93, 92, 3014526875, 67, 93, 90, 92, 2, 90, 93, 91, 93, 89, 91, 92, 92, 91, 91, 68, 1, 92, 93, 91, 91, 91, 86, 93, 86, 90, 93, 0, 91, 92, 93, 93, 1, 87, 273949728, 77, 1, 86, 2085081184, 1, 87, 3354365294, 77, 0, 52, 62, 4256609582, 68, 1, 91, 91, 93, 91, 91, 23, 92, 53, 52, 92, 1, 91, 92, 93, 93, 61, 23, 93, 93, 93, 92, 53, 93, 93, 69, 92, 3423647735, 63, 21, 92, 92, 23, 54, 91, 64, 54, 93, 16, 93, 92, 93, 92, 93, 91, 92, 3486481077, 92, 89, 65, 91, 91, 93, 64, 89, 91, 89, 91, 55732975, 1, 86, 2133396260, 1, 90, 2770747192, 66, 91, 65, 90, 90, 93, 86, 91, 93, 3208576186, 67, 91, 89, 91, 0, 90, 92, 93, 93, 90, 91, 91, 91, 91, 93, 68, 0, 91, 93, 93, 92, 91, 86, 91, 86, 90, 93, 1, 91, 91, 92, 91, 1, 87, 4175859681, 77, 1, 86, 3359698223, 1, 87, 152823078, 77, 0, 55, 66, 1851010751, 66, 91, 67, 56, 23, 93, 55, 93, 93, 1166166742, 61, 23, 93, 91, 91, 93, 56, 93, 91, 69, 92, 3910425401, 36, 21, 92, 91, 23, 57, 93, 64, 57, 93, 31, 93, 91, 91, 93, 91, 93, 93, 827490508, 93, 89, 65, 92, 93, 91, 68, 89, 92, 89, 93, 2897696061, 62, 89, 89, 91, 93, 92, 91, 92, 1, 86, 1848181585, 1, 90, 2485602250, 66, 93, 69, 90, 90, 91, 86, 93, 91, 3073429379, 70, 45078, 1568, 91, 94, 89, 3646337835, 90, 91, 91, 93, 91, 90, 70, 68, 1, 91, 93, 93, 93, 91, 86, 91, 86, 90, 91, 1, 91, 93, 91, 91, 1, 87, 994663123, 77, 1, 86, 2014439286, 1, 87, 1510156105, 77, 0, 58, 71, 2886425640, 66, 92, 72, 59, 23, 93, 58, 93, 93, 3443383285, 61, 23, 92, 93, 93, 93, 59, 92, 91, 69, 91, 2628385510, 73, 21, 93, 92, 23, 60, 91, 64, 60, 93, 16, 92, 91, 93, 93, 92, 91, 91, 3486481077, 93, 89, 65, 93, 93, 93, 74, 89, 92, 89, 92, 3200300252, 62, 89, 89, 91, 93, 92, 93, 93, 1, 86, 2542017639, 1, 90, 3048781167, 66, 93, 75, 90, 90, 92, 86, 93, 92, 700637997, 70, 38865, 1691, 92, 94, 90, 1458109738, 89, 91, 93, 92, 93, 90, 76, 68, 1, 91, 93, 91, 91, 93, 86, 92, 86, 90, 91, 0, 93, 91, 91, 91, 1, 87, 1193457527, 77, 1, 86, 3889367178, 1, 87, 394344277, 77, 0, 61, 77, 259978658, 66, 91, 78, 62, 23, 93, 61, 93, 93, 1608055797, 61, 23, 93, 92, 91, 93, 62, 92, 93, 69, 92, 2388876876, 79, 21, 91, 92, 23, 63, 92, 64, 63, 92, 80, 91, 91, 91, 91, 93, 92, 93, 2361736016, 91, 89, 65, 93, 92, 92, 81, 89, 92, 89, 92, 1388805362, 1, 86, 4222276714, 1, 90, 564199163, 66, 93, 82, 90, 90, 92, 86, 92, 91, 2992318119, 67, 92, 90, 91, 2, 90, 91, 93, 93, 89, 91, 93, 93, 91, 93, 68, 0, 91, 91, 92, 93, 91, 86, 93, 86, 90, 93, 0, 91, 93, 93, 91, 1, 87, 3966342149, 77, 1, 86, 4035292970, 1, 87, 317994030, 77, 0, 64, 83, 125678014, 68, 0, 93, 93, 92, 91, 93, 23, 93, 65, 64, 91, 1, 91, 91, 91, 93, 61, 23, 91, 92, 91, 93, 65, 92, 93, 69, 91, 827490508, 31, 21, 93, 91, 23, 66, 93, 64, 66, 92, 80, 92, 93, 93, 92, 92, 93, 92, 2361736016, 91, 89, 65, 92, 92, 91, 84, 89, 92, 89, 92, 2853741685, 62, 89, 89, 93, 92, 92, 92, 91, 1, 86, 3538477363, 1, 90, 4041153471, 66, 91, 85, 90, 90, 91, 86, 92, 91, 2773243662, 70, 11782, 1937, 92, 94, 90, 3741377601, 89, 92, 93, 92, 91, 90, 86, 68, 0, 93, 92, 91, 93, 92, 86, 92, 86, 90, 93, 1, 93, 91, 93, 91, 1, 87, 1821675737, 77, 1, 86, 2537985717, 1, 87, 2684511710, 77, 0, 67, 87, 329594098, 66, 91, 88, 68, 23, 91, 67, 91, 91, 1623999388, 61, 23, 93, 92, 91, 91, 68, 91, 93, 69, 93, 3496426906, 41, 21, 93, 93, 23, 69, 93, 64, 69, 93, 89, 91, 91, 91, 93, 93, 93, 91, 3487370438, 92, 89, 65, 91, 91, 92, 90, 89, 93, 89, 93, 3433364653, 1, 86, 1711836133, 1, 90, 2214443049, 66, 93, 91, 90, 90, 91, 86, 91, 93, 2662854893, 67, 92, 89, 91, 2, 90, 93, 91, 92, 90, 91, 91, 93, 92, 91, 68, 1, 93, 93, 91, 91, 91, 86, 93, 86, 90, 92, 1, 93, 92, 91, 93, 1, 87, 3903066288, 77, 1, 86, 759165984, 1, 87, 3279268756, 77, 0, 70, 92, 3555469620, 68, 0, 92, 92, 92, 91, 91, 23, 93, 71, 70, 92, 2, 92, 92, 91, 92, 61, 23, 91, 91, 92, 93, 71, 93, 93, 69, 92, 3487370438, 89, 21, 91, 92, 23, 72, 91, 64, 72, 93, 80, 93, 91, 93, 91, 91, 92, 91, 2361736016, 91, 89, 65, 93, 93, 91, 93, 89, 93, 89, 92, 1493769187, 62, 89, 89, 93, 93, 93, 91, 91, 1, 86, 1309210895, 1, 90, 1677599039, 66, 92, 94, 90, 90, 91, 86, 91, 92, 3337320860, 70, 33207, 2183, 92, 94, 90, 2142073663, 89, 93, 91, 92, 93, 90, 95, 68, 2, 92, 91, 92, 93, 92, 86, 91, 86, 90, 92, 0, 91, 93, 91, 93, 1, 87, 1309454058, 77, 1, 86, 1840538921, 1, 87, 4027509455, 77, 0, 73, 96, 1262364156, 66, 91, 97, 74, 23, 92, 73, 92, 93, 1855697090, 61, 23, 91, 93, 91, 91, 74, 93, 91, 69, 92, 67257012, 20, 21, 91, 92, 23, 75, 92, 64, 75, 93, 55, 93, 93, 91, 93, 92, 92, 91, 1237309130, 92, 89, 65, 92, 92, 93, 98, 89, 92, 89, 93, 3040416287, 62, 89, 89, 92, 92, 93, 91, 93, 1, 86, 351715654, 1, 90, 721223797, 66, 92, 99, 90, 90, 93, 86, 93, 91, 1290313650, 70, 32519, 2306, 92, 94, 90, 3280596064, 89, 91, 91, 92, 91, 90, 100, 68, 2, 91, 91, 91, 93, 93, 86, 91, 86, 90, 92, 0, 91, 91, 91, 91, 1, 87, 3095508768, 77, 1, 86, 3934723694, 1, 87, 2584826931, 77, 0, 76, 101, 3955825337, 68, 0, 92, 92, 92, 92, 93, 23, 92, 77, 76, 92, 0, 93, 92, 93, 92, 61, 23, 93, 91, 92, 91, 77, 91, 92, 69, 93, 1863010249, 102, 21, 91, 91, 23, 78, 93, 64, 78, 91, 89, 93, 91, 93, 93, 93, 93, 91, 3487370438, 93, 89, 65, 93, 93, 92, 103, 89, 93, 89, 93, 2377261460, 62, 89, 89, 92, 92, 91, 92, 92, 1, 86, 387710468, 1, 90, 1025592216, 66, 91, 104, 90, 90, 93, 86, 91, 93, 3629482305, 70, 12843, 2436, 92, 94, 90, 141601920, 89, 93, 92, 92, 91, 90, 105, 68, 2, 91, 93, 92, 93, 92, 86, 92, 86, 90, 93, 2, 91, 92, 91, 91, 1, 87, 4186244693, 77, 1, 86, 4016613604, 1, 87, 160502587, 77, 0, 79, 106, 2202530382, 66, 92, 107, 80, 23, 92, 79, 92, 93, 1334549428, 61, 23, 91, 91, 91, 91, 80, 92, 92, 69, 92, 1328963825, 108, 21, 92, 92, 23, 81, 92, 64, 81, 93, 31, 92, 92, 92, 91, 92, 92, 93, 827490508, 92, 89, 65, 91, 93, 91, 109, 89, 92, 89, 93, 4206231275, 62, 89, 89, 93, 93, 93, 93, 92, 1, 86, 4279885504, 1, 90, 621672470, 66, 91, 110, 90, 90, 91, 86, 93, 93, 2550659839, 70, 20845, 2559, 91, 94, 90, 3517256414, 89, 93, 92, 92, 92, 90, 111, 68, 0, 93, 92, 91, 93, 92, 86, 93, 86, 90, 92, 2, 93, 91, 92, 91, 1, 87, 1090263209, 77, 1, 86, 3214205446, 1, 87, 4204313540, 77, 0, 82, 112, 1439015184, 66, 92, 113, 83, 23, 93, 82, 93, 91, 3732861693, 61, 23, 93, 92, 93, 92, 83, 93, 92, 69, 93, 3496426906, 41, 21, 92, 93, 23, 84, 93, 64, 84, 92, 37, 91, 92, 91, 92, 91, 92, 93, 3830473707, 91, 89, 65, 92, 92, 91, 114, 89, 92, 89, 93, 2986155080, 1, 86, 262983893, 1, 90, 766213018, 66, 92, 115, 90, 90, 92, 86, 91, 92, 1472318458, 67, 92, 90, 92, 2, 90, 92, 91, 92, 89, 93, 92, 92, 93, 91, 68, 1, 93, 91, 92, 93, 92, 86, 91, 86, 90, 92, 2, 92, 93, 92, 93, 1, 87, 1137588970, 77, 1, 86, 4191194131, 1, 87, 249720495, 77, 1, 86, 2370760215, 1, 87, 3912301872, 77, 61, 5, 92, 92, 92, 92, 9, 92, 92, 1, 4, 3221, 55, 1, 85, 11627, 68, 1, 92, 91, 93, 91, 92, 21, 92, 21, 85, 91, 2, 93, 93, 92, 92, 1, 86, 790971562, 1, 87, 1197120923, 77, 80, 42, 8, 15, 4, 14, 16, 17, 18, 44, 96, 7, 1, 8, 96, 97, 98, 0, 6, 9, 420988975, 1, 85, 27846, 66, 93, 116, 21, 21, 91, 85, 91, 93, 2947754694, 1, 86, 2808102237, 1, 87, 3474959560, 77, 55, 1, 85, 15897, 66, 92, 117, 21, 21, 91, 85, 91, 92, 340246975, 1, 86, 693230928, 1, 87, 1294861358, 77, 1, 21, 7739, 81, 42, 10, 15, 4, 14, 16, 17, 18, 99, 61, 5, 91, 93, 91, 91, 96, 92, 93, 1, 85, 14767, 66, 92, 118, 21, 21, 93, 85, 93, 93, 2757680399, 1, 86, 2008246352, 1, 87, 4274812104, 77, 1, 21, 45670, 1, 16, 2536443224, 1, 17, 265, 1, 18, 7, 1, 85, 18120, 68, 1, 91, 92, 92, 93, 93, 21, 93, 21, 85, 92, 1, 92, 93, 93, 93, 1, 86, 3911310838, 1, 87, 2371752079, 77, 100, 1, 16, 2459196070, 1, 17, 255, 1, 85, 417, 68, 1, 91, 91, 91, 92, 92, 21, 92, 21, 85, 92, 0, 91, 91, 91, 92, 1, 86, 242400652, 1, 87, 1745692296, 77, 101, 47, 15, 6537, 4, 93, 0, 0, 102, 1, 85, 9851, 68, 0, 91, 92, 91, 92, 92, 21, 92, 21, 85, 92, 1, 91, 91, 93, 92, 1, 86, 167284113, 1, 87, 1820808933, 77, 103, 104, 1, 85, 34700, 66, 92, 119, 21, 21, 92, 85, 93, 92, 3442795933, 1, 86, 676402407, 1, 87, 1311690129, 77, 1, 4, 2812, 55, 1, 85, 33750, 66, 91, 120, 21, 21, 93, 85, 93, 93, 1794502537, 1, 86, 846522263, 1, 87, 1141571395, 77, 1, 85, 48048, 71, 39406, 92, 85, 21, 92, 91, 92, 85, 64, 85, 92, 36, 92, 93, 93, 92, 92, 92, 92, 3910425401, 92, 89, 65, 91, 91, 91, 121, 89, 92, 89, 91, 3842158766, 62, 89, 89, 91, 91, 92, 92, 91, 1, 86, 1605500758, 1, 90, 3387380233, 66, 92, 122, 90, 90, 91, 86, 91, 92, 2749612710, 70, 9538, 3083, 92, 94, 90, 1584840073, 89, 91, 92, 92, 92, 90, 123, 68, 2, 92, 92, 92, 92, 92, 86, 93, 86, 90, 92, 2, 93, 93, 92, 92, 1, 87, 3299012446, 77, 1, 85, 12771, 66, 92, 124, 21, 21, 93, 85, 93, 93, 1940584257, 1, 86, 2800257246, 1, 87, 3482804547, 77, 1, 85, 8521, 66, 93, 125, 21, 21, 92, 85, 92, 92, 3381732675, 1, 86, 3459706866, 1, 87, 2823356642, 77, 1, 21, 15273, 105, 1, 16, 3976635791, 1, 17, 243, 1, 85, 9920, 68, 0, 93, 92, 91, 91, 91, 21, 92, 21, 85, 91, 1, 92, 91, 92, 92, 1, 86, 348885170, 1, 87, 1639207413, 77, 80, 42, 12, 15, 4, 14, 16, 17, 18, 0, 13, 126, 0, 1, 85, 14081, 68, 0, 93, 93, 93, 91, 91, 21, 92, 21, 85, 91, 1, 93, 93, 92, 91, 1, 86, 1119803893, 1, 87, 868288021, 77, 1, 21, 8022, 81, 0, 96, 5, 3711018866, 68, 0, 93, 92, 91, 91, 91, 0, 93, 9, 96, 91, 0, 91, 93, 91, 92, 55, 1, 85, 26143, 66, 91, 127, 21, 21, 92, 85, 91, 91, 1675775053, 1, 86, 451384508, 1, 87, 1536708321, 77, 71, 61479, 93, 96, 0, 93, 93, 93, 6, 64, 96, 93, 80, 91, 93, 93, 93, 93, 91, 93, 2361736016, 92, 89, 65, 93, 93, 91, 128, 89, 93, 89, 92, 1005780783, 1, 86, 2903538431, 1, 90, 3780292337, 66, 93, 129, 90, 90, 93, 86, 92, 93, 2548929872, 67, 93, 89, 92, 2, 90, 93, 92, 93, 90, 91, 93, 92, 92, 91, 68, 2, 93, 92, 93, 92, 92, 86, 93, 86, 90, 91, 2, 93, 92, 92, 93, 1, 87, 1988421428, 77, 1, 85, 28192, 68, 2, 91, 92, 93, 93, 92, 21, 91, 21, 85, 93, 0, 93, 93, 92, 93, 1, 86, 2622090809, 1, 87, 3660970998, 77, 1, 85, 56646, 68, 0, 91, 92, 91, 91, 91, 21, 91, 21, 85, 91, 0, 92, 91, 92, 93, 1, 86, 3809456553, 1, 87, 2473600090, 77, 55, 1, 4, 3437, 1, 85, 11000, 68, 1, 92, 93, 92, 91, 91, 21, 92, 21, 85, 91, 2, 93, 91, 92, 92, 1, 86, 2025365783, 1, 87, 4257693015, 77, 1, 21, 28816, 0, 96, 7, 0, 106, 1, 21, 4574, 107, 42, 86, 88, 2, 87, 86, 108, 0, 81, 9, 420988975, 0, 80, 130, 553084399, 0, 77, 131, 1829834018, 0, 78, 132, 2062759255, 0, 79, 133, 1328464344, 47, 73, 10011, 3, 15, 0, 0, 109, 0, 16, 134, 2368225490, 61, 17, 79, 78, 78, 79, 16, 78, 78, 0, 18, 9, 420988975, 61, 19, 79, 79, 79, 79, 18, 79, 78, 0, 20, 135, 371332738, 63, 17, 20, 78, 79, 78, 79, 79, 21, 25915, 3568829233, 78, 78, 136, 79, 64, 21, 78, 102, 78, 77, 78, 78, 77, 78, 79, 1863010249, 78, 75, 65, 78, 78, 79, 137, 75, 79, 75, 77, 2886117834, 62, 75, 75, 77, 78, 78, 79, 78, 1, 70, 3824400635, 1, 76, 4080908434, 66, 77, 138, 76, 76, 77, 70, 78, 78, 4253463074, 70, 13638, 3591, 79, 80, 76, 217464467, 75, 78, 79, 77, 77, 76, 139, 68, 1, 77, 77, 79, 77, 79, 70, 77, 70, 76, 77, 1, 78, 79, 77, 77, 1, 71, 49355, 1, 72, 51099, 79, 0, 22, 140, 488463442, 61, 19, 79, 79, 79, 79, 22, 79, 79, 69, 79, 1328963825, 108, 17, 79, 79, 19, 23, 78, 64, 23, 79, 16, 77, 79, 78, 79, 79, 78, 79, 3486481077, 77, 75, 65, 79, 78, 77, 141, 75, 77, 75, 78, 38253920, 62, 75, 75, 77, 78, 79, 78, 78, 1, 70, 1591501973, 1, 76, 3143990550, 66, 79, 142, 76, 76, 77, 70, 79, 78, 105939652, 70, 54548, 3700, 78, 80, 75, 4191359255, 76, 79, 78, 78, 79, 76, 143, 68, 2, 78, 77, 78, 78, 77, 70, 77, 70, 76, 78, 0, 78, 79, 78, 79, 1, 71, 51561, 1, 72, 2594, 79, 1, 70, 1512200255, 1, 71, 54093, 1, 72, 58560, 79, 0, 24, 144, 4071676977, 68, 0, 79, 79, 79, 77, 77, 19, 79, 25, 24, 77, 0, 79, 77, 78, 79, 61, 19, 79, 78, 77, 77, 25, 79, 77, 69, 78, 1117680260, 47, 17, 78, 79, 19, 26, 79, 64, 26, 77, 20, 77, 78, 77, 77, 79, 79, 78, 67257012, 78, 75, 65, 77, 78, 77, 145, 75, 77, 75, 79, 2282962773, 62, 75, 75, 79, 78, 78, 78, 79, 1, 70, 717803581, 1, 76, 2270291951, 66, 78, 146, 76, 76, 77, 70, 77, 79, 1156222290, 70, 17441, 3836, 78, 80, 75, 2248280518, 76, 77, 77, 79, 78, 76, 147, 68, 2, 79, 78, 78, 79, 78, 70, 78, 70, 76, 78, 0, 78, 77, 78, 77, 1, 71, 62057, 1, 72, 23524, 79, 1, 70, 1883827599, 1, 71, 40460, 1, 72, 44230, 79, 0, 27, 148, 2789592607, 68, 1, 79, 79, 77, 79, 79, 19, 78, 28, 27, 79, 2, 77, 79, 77, 77, 61, 19, 77, 77, 77, 79, 28, 77, 79, 69, 79, 884690417, 25, 17, 77, 78, 19, 29, 78, 64, 29, 77, 31, 77, 79, 78, 77, 78, 77, 77, 827490508, 77, 75, 65, 78, 77, 79, 149, 75, 78, 75, 79, 3866897976, 62, 75, 75, 77, 79, 79, 78, 79, 1, 70, 1403120086, 1, 76, 2955606068, 66, 78, 150, 76, 76, 78, 70, 78, 78, 159445689, 70, 2532, 3972, 78, 80, 75, 746111656, 76, 78, 79, 77, 77, 76, 151, 68, 0, 79, 78, 77, 77, 79, 70, 78, 70, 76, 79, 0, 79, 77, 77, 79, 1, 71, 27793, 1, 72, 20666, 79, 1, 70, 1331816170, 1, 71, 15892, 1, 72, 13377, 79, 0, 30, 152, 2587148294, 66, 79, 153, 31, 19, 77, 30, 77, 79, 1654548021, 61, 19, 79, 77, 79, 77, 31, 78, 77, 69, 78, 3423647735, 63, 17, 79, 78, 19, 32, 77, 64, 32, 78, 55, 78, 77, 77, 79, 78, 77, 79, 1237309130, 78, 75, 65, 78, 77, 79, 154, 75, 77, 75, 77, 38791558, 1, 70, 3051996392, 1, 76, 309515042, 66, 78, 155, 76, 76, 78, 70, 78, 78, 1614293233, 67, 77, 75, 79, 0, 76, 78, 77, 79, 76, 78, 78, 77, 77, 77, 68, 0, 79, 79, 78, 78, 79, 70, 79, 70, 76, 77, 0, 77, 77, 77, 79, 1, 71, 43631, 1, 72, 48583, 79, 1, 70, 3093116516, 1, 71, 28277, 1, 72, 57401, 79, 0, 33, 156, 2317002395, 68, 2, 77, 79, 78, 79, 79, 19, 77, 34, 33, 79, 1, 79, 79, 79, 78, 61, 19, 77, 79, 79, 77, 34, 79, 78, 69, 79, 3496426906, 41, 17, 77, 78, 19, 35, 77, 64, 35, 77, 47, 79, 77, 78, 77, 77, 79, 77, 1117680260, 77, 75, 65, 77, 77, 78, 157, 75, 77, 75, 78, 2644679494, 1, 70, 2458140457, 1, 76, 4010625375, 66, 78, 158, 76, 76, 78, 70, 78, 79, 739949379, 67, 78, 76, 77, 1, 76, 79, 77, 78, 75, 78, 78, 78, 78, 77, 68, 2, 77, 78, 78, 78, 78, 70, 78, 70, 76, 78, 1, 78, 78, 78, 77, 1, 71, 1002, 1, 72, 22447, 79, 1, 70, 2940543270, 1, 71, 55945, 1, 72, 61796, 79, 0, 36, 159, 458486642, 66, 79, 160, 37, 19, 79, 36, 79, 77, 1150213407, 61, 19, 78, 79, 79, 79, 37, 79, 79, 69, 78, 67257012, 20, 17, 77, 79, 19, 38, 79, 64, 38, 77, 32, 77, 77, 79, 79, 78, 77, 77, 1563209287, 77, 75, 65, 78, 79, 79, 161, 75, 77, 75, 79, 3973550927, 62, 75, 75, 79, 77, 77, 79, 78, 1, 70, 1342138415, 1, 76, 2894625246, 66, 79, 162, 76, 76, 77, 70, 79, 77, 387079483, 70, 60635, 4352, 79, 80, 75, 3738552935, 76, 79, 77, 77, 79, 76, 163, 68, 1, 77, 79, 77, 79, 79, 70, 77, 70, 76, 79, 1, 79, 77, 77, 77, 1, 71, 43336, 1, 72, 4277, 79, 1, 70, 3459823949, 1, 71, 40246, 1, 72, 42586, 79, 0, 39, 164, 3806973077, 68, 0, 79, 79, 79, 79, 79, 19, 79, 40, 39, 79, 0, 79, 78, 79, 79, 61, 19, 78, 79, 79, 79, 40, 79, 77, 69, 77, 1237309130, 55, 17, 79, 78, 19, 41, 78, 64, 41, 79, 102, 77, 77, 79, 78, 77, 79, 78, 1863010249, 78, 75, 65, 78, 78, 77, 165, 75, 78, 75, 78, 3886148350, 1, 70, 3862414075, 1, 76, 1119935010, 66, 77, 166, 76, 76, 78, 70, 78, 78, 1829224475, 67, 78, 75, 77, 0, 76, 78, 79, 77, 76, 77, 78, 77, 77, 77, 68, 2, 79, 78, 77, 79, 77, 70, 78, 70, 76, 78, 1, 78, 77, 78, 77, 1, 71, 8639, 1, 72, 61695, 79, 1, 70, 582397749, 1, 71, 34819, 1, 72, 37774, 79, 0, 42, 167, 668609511, 68, 2, 79, 77, 79, 78, 78, 19, 77, 43, 42, 78, 0, 79, 79, 79, 79, 61, 19, 78, 79, 79, 77, 43, 78, 78, 69, 77, 67257012, 20, 17, 79, 79, 19, 44, 77, 64, 44, 77, 25, 77, 78, 77, 78, 78, 77, 79, 884690417, 79, 75, 65, 78, 77, 78, 168, 75, 78, 75, 78, 2723266510, 62, 75, 75, 77, 78, 78, 79, 77, 1, 70, 1437995183, 1, 76, 2990481917, 66, 79, 169, 76, 76, 78, 70, 78, 78, 1993244305, 70, 38930, 4617, 79, 80, 76, 1245003063, 75, 77, 79, 77, 79, 76, 170, 68, 1, 79, 77, 79, 77, 77, 70, 78, 70, 76, 79, 1, 77, 79, 77, 79, 1, 71, 27023, 1, 72, 40751, 79, 1, 70, 382760922, 1, 71, 46696, 1, 72, 63727, 79, 0, 45, 171, 1004719484, 66, 77, 172, 46, 19, 77, 45, 77, 79, 2887170312, 61, 19, 78, 77, 77, 77, 46, 77, 77, 69, 78, 1863010249, 102, 17, 77, 78, 19, 47, 78, 64, 47, 77, 79, 77, 78, 78, 78, 78, 77, 77, 2388876876, 78, 75, 65, 78, 78, 78, 173, 75, 79, 75, 79, 591342041, 62, 75, 75, 78, 77, 77, 79, 78, 1, 70, 427970364, 1, 76, 1980457450, 66, 78, 174, 76, 76, 79, 70, 79, 77, 1129998863, 70, 2701, 4746, 79, 80, 76, 1163933107, 75, 78, 79, 79, 79, 76, 175, 68, 1, 78, 78, 77, 77, 79, 70, 78, 70, 76, 79, 1, 77, 78, 77, 79, 1, 71, 65426, 1, 72, 20964, 79, 1, 70, 3880891551, 1, 71, 5122, 1, 72, 41917, 79, 0, 48, 176, 1504756092, 68, 0, 77, 77, 79, 77, 77, 19, 78, 49, 48, 77, 0, 79, 77, 77, 77, 61, 19, 78, 77, 77, 79, 49, 77, 79, 69, 77, 1328963825, 108, 17, 79, 78, 19, 50, 78, 64, 50, 77, 89, 79, 79, 77, 78, 78, 78, 79, 3487370438, 77, 75, 65, 78, 77, 79, 177, 75, 79, 75, 77, 3161478260, 1, 70, 3638889530, 1, 76, 896408780, 66, 77, 178, 76, 76, 79, 70, 79, 77, 4158706796, 67, 79, 75, 77, 1, 76, 79, 78, 79, 76, 77, 77, 77, 79, 77, 68, 1, 78, 79, 79, 79, 77, 70, 78, 70, 76, 79, 1, 79, 79, 78, 78, 1, 71, 6262, 1, 72, 2308, 79, 1, 70, 3803627653, 1, 71, 24064, 1, 72, 6388, 79, 0, 51, 179, 487216468, 66, 78, 180, 52, 19, 78, 51, 77, 77, 3191842779, 61, 19, 78, 78, 78, 78, 52, 78, 78, 69, 79, 3486481077, 16, 17, 78, 78, 19, 53, 79, 64, 53, 77, 26, 77, 79, 79, 78, 77, 79, 79, 3167482679, 78, 75, 65, 79, 79, 78, 181, 75, 78, 75, 77, 1780165427, 62, 75, 75, 77, 77, 77, 77, 77, 1, 70, 2591244771, 1, 76, 4143730733, 66, 78, 182, 76, 76, 79, 70, 77, 77, 4099338181, 70, 18325, 5004, 77, 80, 75, 2678217582, 76, 77, 77, 77, 77, 76, 183, 68, 1, 78, 78, 78, 79, 77, 70, 77, 70, 76, 79, 2, 77, 79, 77, 78, 1, 71, 45620, 1, 72, 48631, 79, 1, 70, 2457748861, 1, 71, 26196, 1, 72, 107, 79, 0, 54, 184, 2502674604, 68, 0, 78, 78, 77, 77, 77, 19, 78, 55, 54, 77, 2, 78, 78, 79, 78, 61, 19, 77, 79, 77, 78, 55, 78, 78, 69, 79, 1328963825, 108, 17, 79, 78, 19, 56, 79, 64, 56, 77, 73, 79, 77, 78, 77, 77, 78, 79, 2628385510, 78, 75, 65, 78, 77, 79, 185, 75, 77, 75, 77, 1628593587, 62, 75, 75, 78, 77, 78, 78, 78, 1, 70, 3525126472, 1, 76, 782648119, 66, 79, 186, 76, 76, 79, 70, 77, 79, 2954399805, 70, 15590, 5140, 79, 80, 75, 2511561751, 76, 78, 78, 77, 79, 76, 187, 68, 1, 78, 77, 78, 78, 79, 70, 78, 70, 76, 79, 1, 77, 78, 77, 79, 1, 71, 23444, 1, 72, 41980, 79, 1, 70, 3365030709, 1, 71, 29527, 1, 72, 36547, 79, 0, 57, 188, 1795758525, 66, 79, 189, 58, 19, 79, 57, 79, 78, 3342049173, 61, 19, 79, 78, 77, 78, 58, 78, 79, 69, 77, 1563209287, 32, 17, 78, 79, 19, 59, 79, 64, 59, 77, 108, 78, 79, 78, 79, 78, 79, 79, 1328963825, 79, 75, 65, 77, 77, 78, 190, 75, 79, 75, 79, 2172303339, 1, 70, 1575546345, 1, 76, 3128034467, 66, 77, 191, 76, 76, 77, 70, 79, 78, 1128756234, 67, 79, 75, 77, 2, 76, 78, 79, 79, 76, 78, 77, 77, 79, 77, 68, 0, 78, 77, 77, 78, 77, 70, 78, 70, 76, 79, 1, 79, 78, 79, 79, 1, 71, 45120, 1, 72, 59269, 79, 1, 70, 2018370404, 1, 71, 54986, 1, 72, 62681, 79, 0, 60, 192, 2966715723, 66, 78, 193, 61, 19, 79, 60, 79, 79, 2288184399, 61, 19, 79, 79, 79, 77, 61, 79, 79, 69, 79, 3910425401, 36, 17, 78, 79, 19, 62, 79, 64, 62, 77, 20, 79, 77, 78, 79, 77, 78, 79, 67257012, 78, 75, 65, 77, 79, 77, 194, 75, 78, 75, 78, 1649119251, 1, 70, 1685047949, 1, 76, 3237534239, 66, 77, 195, 76, 76, 77, 70, 78, 77, 3024488699, 67, 78, 75, 77, 0, 76, 77, 79, 77, 76, 78, 77, 77, 77, 78, 68, 2, 79, 78, 77, 78, 79, 70, 78, 70, 76, 78, 2, 77, 78, 78, 78, 1, 71, 59762, 1, 72, 45139, 79, 1, 70, 2925127781, 1, 71, 37567, 1, 72, 3199, 79, 0, 63, 196, 2809231387, 68, 1, 78, 78, 78, 78, 78, 19, 78, 64, 63, 78, 0, 79, 78, 77, 78, 61, 19, 77, 79, 79, 79, 64, 78, 78, 69, 79, 3496426906, 41, 17, 78, 79, 19, 65, 79, 64, 65, 77, 89, 79, 78, 79, 79, 79, 79, 79, 3487370438, 77, 75, 65, 78, 78, 79, 197, 75, 79, 75, 77, 1765446220, 1, 70, 2779511286, 1, 76, 37031764, 66, 79, 198, 76, 76, 79, 70, 77, 77, 3663264681, 67, 79, 75, 77, 1, 76, 79, 77, 79, 76, 78, 77, 79, 78, 78, 68, 2, 79, 78, 78, 77, 79, 70, 79, 70, 76, 77, 2, 77, 77, 78, 79, 1, 71, 35382, 1, 72, 58777, 79, 1, 70, 1189994295, 1, 71, 59703, 1, 72, 53588, 79, 0, 66, 199, 667352497, 66, 77, 200, 67, 19, 77, 66, 77, 77, 364424264, 61, 19, 77, 79, 77, 77, 67, 79, 78, 69, 78, 827490508, 31, 17, 77, 79, 19, 68, 78, 64, 68, 77, 79, 78, 79, 78, 79, 78, 77, 78, 2388876876, 79, 75, 65, 78, 77, 78, 201, 75, 78, 75, 79, 1494387078, 62, 75, 75, 77, 79, 79, 78, 78, 1, 70, 2612102697, 1, 76, 4164588191, 66, 79, 202, 76, 76, 78, 70, 79, 78, 2540995309, 70, 60089, 5642, 78, 80, 75, 1112366927, 76, 77, 77, 78, 77, 76, 203, 68, 1, 79, 79, 78, 78, 78, 70, 77, 70, 76, 77, 1, 79, 78, 78, 77, 1, 71, 55310, 1, 72, 26133, 79, 1, 70, 4095304140, 1, 71, 43103, 1, 72, 49204, 79, 1, 70, 1108965727, 1, 71, 50183, 1, 72, 40406, 79, 0, 82, 204, 83370701, 68, 2, 79, 79, 79, 79, 79, 5, 79, 83, 82, 79, 2, 79, 79, 77, 77, 61, 5, 79, 77, 77, 79, 83, 79, 77, 1, 69, 23866, 66, 78, 205, 17, 17, 79, 69, 77, 79, 388140902, 1, 70, 193379175, 1, 71, 33819, 1, 72, 54377, 79, 0, 82, 7, 0, 110, 0, 82, 206, 95854227, 111, 0, 13, 5, 3711018866, 1, 69, 2397, 66, 77, 207, 17, 17, 77, 69, 77, 78, 2448219867, 1, 70, 2136030933, 1, 71, 551, 1, 72, 24735, 79, 45, 4, 1, 69, 43384, 71, 41400, 78, 69, 17, 79, 79, 78, 69, 64, 69, 78, 31, 77, 78, 77, 78, 79, 77, 78, 827490508, 77, 75, 65, 78, 77, 78, 208, 75, 77, 75, 79, 2345142920, 1, 70, 3960965830, 1, 76, 3598471387, 66, 77, 209, 76, 76, 78, 70, 78, 79, 4176649138, 67, 79, 75, 77, 2, 76, 79, 78, 79, 76, 77, 77, 77, 77, 78, 68, 2, 78, 77, 77, 77, 77, 70, 78, 70, 76, 78, 2, 78, 77, 77, 79, 1, 71, 50997, 1, 72, 7004, 79, 1, 69, 17170, 68, 1, 77, 77, 79, 79, 79, 17, 79, 17, 69, 79, 1, 77, 77, 77, 79, 1, 70, 4141945185, 1, 71, 10368, 1, 72, 59663, 79, 1, 69, 3689, 68, 0, 77, 78, 77, 78, 78, 17, 77, 17, 69, 78, 1, 77, 78, 78, 79, 1, 70, 3527091821, 1, 71, 7770, 1, 72, 3509, 79, 68, 2, 77, 78, 77, 77, 77, 5, 78, 82, 13, 77, 2, 79, 78, 79, 79, 43, 12, 3, 11, 1, 82, 0, 14, 210, 487953511, 1, 69, 5708, 68, 0, 77, 77, 79, 78, 77, 17, 77, 17, 69, 78, 1, 79, 77, 79, 77, 1, 70, 2414617801, 1, 71, 7840, 1, 72, 20484, 79, 0, 82, 211, 1398458961, 112, 71, 18968, 79, 82, 5, 78, 79, 78, 83, 64, 82, 78, 32, 79, 79, 79, 79, 79, 79, 79, 1563209287, 79, 75, 65, 79, 78, 78, 212, 75, 79, 75, 78, 1566360187, 1, 70, 1893479483, 1, 76, 1530987173, 66, 77, 213, 76, 76, 79, 70, 78, 78, 1883407338, 67, 78, 75, 78, 1, 76, 77, 77, 79, 76, 79, 78, 78, 77, 77, 68, 2, 79, 77, 78, 79, 77, 70, 79, 70, 76, 79, 1, 79, 79, 79, 77, 1, 71, 40912, 1, 72, 11734, 79, 1, 69, 24893, 68, 2, 78, 78, 77, 78, 77, 17, 77, 17, 69, 79, 2, 77, 77, 79, 79, 1, 70, 2174078968, 1, 71, 29538, 1, 72, 24154, 79, 1, 69, 13193, 68, 0, 79, 78, 78, 79, 79, 17, 78, 17, 69, 78, 1, 78, 79, 78, 78, 1, 70, 4160142150, 1, 71, 33373, 1, 72, 59385, 79, 0, 82, 206, 95854227, 8, 83, 3, 82, 43, 10, 3, 83, 1, 5, 1, 69, 8858, 68, 0, 78, 79, 78, 79, 79, 17, 78, 17, 69, 77, 2, 78, 77, 79, 79, 1, 70, 1405501558, 1, 71, 64622, 1, 72, 35881, 79, 0, 82, 9, 420988975, 61, 5, 79, 78, 78, 78, 82, 78, 78, 1, 69, 41261, 66, 77, 214, 17, 17, 77, 69, 78, 79, 2863031125, 1, 70, 914453166, 1, 71, 52284, 1, 72, 43374, 79, 2, 7, 26, 3167482679, 0, 82, 215, 3003232955, 8, 8, 7, 82, 1, 69, 14361, 68, 1, 78, 77, 79, 77, 77, 17, 77, 17, 69, 77, 0, 79, 78, 77, 78, 1, 70, 2742064394, 1, 71, 44869, 1, 72, 15487, 79, 4, 2, 2, 82, 216, 3647070605, 42, 6, 82, 1, 0, 1, 69, 14767, 66, 78, 217, 17, 17, 78, 69, 78, 78, 1975137242, 1, 70, 3048612377, 1, 71, 6050, 1, 72, 10810, 79, 1, 69, 32670, 68, 1, 79, 79, 78, 78, 77, 17, 77, 17, 69, 78, 1, 79, 78, 78, 78, 1, 70, 1607796695, 1, 71, 14893, 1, 72, 32795, 79, 1, 69, 12890, 68, 0, 79, 77, 77, 79, 79, 17, 79, 17, 69, 79, 0, 78, 79, 79, 79, 1, 70, 3390567405, 1, 71, 18841, 1, 72, 5592, 79, 72, 78, 77, 79, 78, 79, 79, 12, 78, 78, 82, 14, 79, 78, 73, 83, 79, 78, 81, 10, 218, 82, 3572216551, 78, 78, 3650113602, 6384, 79, 219, 43, 9, 7, 8, 1, 83, 1, 69, 1312, 68, 1, 79, 78, 79, 78, 77, 17, 79, 17, 69, 79, 2, 79, 79, 78, 78, 1, 70, 2483620958, 1, 71, 13912, 1, 72, 19431, 79, 61, 3, 78, 79, 78, 77, 6, 77, 79, 0, 82, 220, 587094060, 61, 4, 77, 79, 78, 79, 82, 79, 79, 1, 69, 1038, 68, 1, 78, 78, 79, 77, 77, 17, 78, 17, 69, 77, 2, 79, 79, 79, 79, 1, 70, 648312899, 1, 71, 50656, 1, 72, 47435, 79, 68, 0, 78, 79, 77, 77, 77, 4, 78, 82, 9, 79, 0, 78, 78, 79, 79, 61, 4, 77, 78, 78, 78, 82, 78, 78, 1, 69, 17169, 66, 78, 221, 17, 17, 79, 69, 79, 77, 3833015731, 1, 70, 1732714290, 1, 71, 9038, 1, 72, 30889, 79, 42, 70, 73, 3, 70, 71, 72, 0, 74, 222, 114532842, 8, 70, 70, 74, 113, 0, 90, 9, 420988975, 0, 88, 223, 827414447, 0, 89, 224, 2233606933, 0, 87, 225, 1993846880, 47, 83, 10311, 2, 11, 0, 0, 4, 20, 0, 21, 226, 1927401035, 61, 22, 88, 87, 88, 88, 21, 88, 87, 0, 23, 9, 420988975, 61, 24, 88, 87, 87, 87, 23, 88, 87, 0, 25, 227, 1541603787, 63, 22, 25, 87, 87, 87, 87, 88, 26, 12078, 2637479480, 87, 88, 228, 87, 64, 26, 88, 80, 88, 87, 88, 88, 87, 88, 88, 2361736016, 88, 85, 65, 87, 88, 88, 229, 85, 88, 85, 88, 1008612131, 1, 81, 2529045487, 1, 86, 2071311369, 66, 87, 230, 86, 86, 88, 81, 88, 88, 1398561021, 67, 88, 86, 87, 1, 86, 88, 88, 88, 85, 87, 87, 88, 87, 87, 68, 2, 88, 87, 87, 87, 87, 81, 87, 81, 86, 87, 1, 88, 87, 87, 87, 1, 82, 2133265041, 78, 0, 27, 231, 4111628074, 61, 24, 88, 88, 88, 87, 27, 88, 88, 69, 87, 884690417, 25, 22, 88, 87, 24, 28, 87, 64, 28, 88, 63, 88, 87, 88, 88, 88, 87, 88, 3423647735, 87, 85, 65, 87, 88, 87, 232, 85, 88, 85, 87, 3710931143, 62, 85, 85, 87, 87, 87, 88, 87, 1, 81, 263460032, 1, 86, 2132938751, 66, 88, 233, 86, 86, 88, 81, 87, 88, 3377647508, 70, 58510, 6768, 88, 89, 85, 3011850909, 86, 87, 88, 87, 87, 86, 234, 68, 2, 87, 87, 87, 87, 88, 81, 88, 81, 86, 88, 1, 87, 88, 87, 88, 1, 82, 3433921334, 78, 1, 81, 3176568664, 1, 82, 191769426, 78, 0, 29, 235, 722182538, 66, 88, 236, 30, 24, 87, 29, 87, 87, 1608945376, 61, 24, 88, 88, 88, 88, 30, 87, 87, 69, 87, 827490508, 31, 22, 87, 88, 24, 31, 88, 64, 31, 88, 32, 87, 87, 87, 87, 87, 88, 87, 1563209287, 88, 85, 65, 87, 87, 87, 237, 85, 88, 85, 88, 1125990044, 62, 85, 85, 88, 87, 87, 87, 87, 1, 81, 1273667068, 1, 86, 3632036545, 66, 87, 238, 86, 86, 88, 81, 87, 88, 2827241815, 70, 33004, 6891, 88, 89, 85, 280732682, 86, 87, 87, 88, 87, 86, 239, 68, 2, 87, 87, 87, 87, 87, 81, 87, 81, 86, 87, 1, 87, 87, 88, 87, 1, 82, 49501799, 78, 1, 81, 2823121013, 1, 82, 2369770607, 78, 0, 32, 240, 1834474887, 66, 88, 241, 33, 24, 88, 32, 88, 88, 1418338959, 61, 24, 87, 87, 87, 88, 33, 88, 88, 69, 87, 1863010249, 102, 22, 87, 87, 24, 34, 88, 64, 34, 87, 73, 87, 87, 88, 88, 88, 87, 87, 2628385510, 88, 85, 65, 88, 87, 87, 242, 85, 88, 85, 87, 504620989, 62, 85, 85, 88, 87, 88, 88, 88, 1, 81, 1001608942, 1, 86, 1913181741, 66, 88, 243, 86, 86, 87, 81, 87, 87, 695417575, 70, 28107, 7014, 87, 89, 86, 1030324468, 85, 88, 88, 87, 87, 86, 244, 68, 1, 88, 87, 88, 87, 87, 81, 87, 81, 86, 87, 2, 88, 87, 87, 87, 1, 82, 4229143206, 78, 1, 81, 392131176, 1, 82, 3645548026, 78, 0, 35, 245, 1487863318, 66, 88, 246, 36, 24, 87, 35, 87, 87, 2411712904, 61, 24, 88, 87, 88, 87, 36, 87, 88, 69, 87, 884690417, 25, 22, 87, 87, 24, 37, 88, 64, 37, 87, 55, 88, 87, 88, 88, 87, 87, 87, 1237309130, 87, 85, 65, 87, 87, 88, 247, 85, 87, 85, 87, 1336425470, 62, 85, 85, 88, 88, 88, 87, 87, 1, 81, 1836540023, 1, 86, 3705510770, 66, 88, 248, 86, 86, 88, 81, 87, 88, 302722680, 70, 31222, 7137, 87, 89, 85, 2153108281, 86, 87, 88, 87, 87, 86, 249, 68, 1, 87, 88, 88, 88, 88, 81, 87, 81, 86, 88, 1, 88, 87, 87, 87, 1, 82, 2973845076, 78, 1, 81, 3391158772, 1, 82, 1353035110, 78, 0, 38, 250, 924289076, 68, 0, 88, 87, 87, 87, 88, 24, 88, 39, 38, 87, 0, 87, 87, 87, 87, 61, 24, 88, 87, 87, 87, 39, 87, 87, 69, 88, 1117680260, 47, 22, 87, 87, 24, 40, 87, 64, 40, 88, 102, 88, 88, 87, 88, 88, 87, 87, 1863010249, 88, 85, 65, 87, 88, 88, 251, 85, 88, 85, 87, 3898634743, 1, 81, 3969502263, 1, 86, 1539640168, 66, 87, 252, 86, 86, 88, 81, 88, 88, 2753330139, 67, 88, 85, 87, 0, 86, 87, 87, 88, 86, 88, 87, 87, 87, 88, 68, 2, 87, 87, 87, 88, 87, 81, 87, 81, 86, 87, 1, 87, 88, 88, 87, 1, 82, 925211155, 78, 1, 81, 3842395956, 1, 82, 2455668248, 78, 0, 41, 253, 1341882955, 66, 87, 254, 42, 24, 87, 41, 87, 87, 3216289046, 61, 24, 88, 87, 87, 87, 42, 87, 87, 69, 88, 3167482679, 26, 22, 87, 87, 24, 43, 88, 64, 43, 87, 79, 87, 88, 87, 87, 87, 87, 87, 2388876876, 87, 85, 65, 87, 88, 88, 255, 85, 88, 85, 88, 1058075091, 1, 81, 173424285, 1, 86, 1887528398, 66, 88, 256, 86, 86, 88, 81, 87, 87, 1387094514, 67, 88, 86, 87, 2, 86, 88, 88, 87, 85, 87, 88, 87, 87, 87, 68, 2, 88, 88, 88, 88, 88, 81, 87, 81, 86, 88, 2, 87, 87, 87, 87, 1, 82, 3069285329, 78, 1, 81, 1505061984, 1, 82, 3479928157, 78, 0, 44, 257, 28280969, 68, 1, 87, 87, 88, 88, 88, 24, 88, 45, 44, 88, 2, 87, 88, 87, 88, 61, 24, 87, 87, 87, 87, 45, 87, 88, 69, 87, 2388876876, 79, 22, 87, 88, 24, 46, 88, 64, 46, 87, 73, 88, 87, 87, 88, 87, 87, 87, 2628385510, 88, 85, 65, 87, 87, 88, 258, 85, 88, 85, 87, 2238235746, 1, 81, 1822248922, 1, 86, 3779485283, 66, 88, 259, 86, 86, 88, 81, 88, 88, 1764591464, 67, 87, 86, 88, 2, 86, 88, 88, 88, 85, 88, 87, 88, 88, 87, 68, 1, 88, 88, 88, 88, 88, 81, 88, 81, 86, 88, 2, 88, 87, 87, 88, 1, 82, 163950657, 78, 1, 81, 2358380444, 1, 82, 2698332972, 78, 0, 47, 260, 1025512770, 66, 87, 261, 48, 24, 87, 47, 88, 87, 3285953444, 61, 24, 87, 87, 87, 87, 48, 87, 87, 69, 87, 2388876876, 79, 22, 87, 87, 24, 49, 87, 64, 49, 88, 63, 88, 88, 88, 87, 87, 87, 87, 3423647735, 87, 85, 65, 88, 88, 88, 262, 85, 87, 85, 88, 1654770967, 62, 85, 85, 88, 88, 88, 87, 88, 1, 81, 1655506604, 1, 86, 1079448283, 66, 87, 263, 86, 86, 88, 81, 87, 87, 1993499958, 70, 16354, 7622, 87, 89, 86, 1263726913, 85, 87, 88, 88, 88, 86, 264, 68, 0, 88, 88, 88, 87, 87, 81, 88, 81, 86, 87, 0, 88, 88, 88, 88, 1, 82, 36883500, 78, 1, 81, 4266056239, 1, 82, 3341439863, 78, 0, 50, 265, 3996675321, 68, 1, 88, 88, 88, 87, 87, 24, 88, 51, 50, 87, 0, 87, 88, 87, 87, 61, 24, 88, 87, 88, 88, 51, 87, 87, 69, 87, 2388876876, 79, 22, 87, 88, 24, 52, 88, 64, 52, 88, 41, 88, 87, 87, 88, 88, 87, 87, 3496426906, 88, 85, 65, 88, 87, 87, 266, 85, 87, 85, 87, 1680104647, 1, 81, 2869550881, 1, 86, 226110152, 66, 88, 267, 86, 86, 88, 81, 87, 88, 379059865, 67, 88, 85, 87, 0, 86, 88, 88, 87, 86, 88, 87, 87, 88, 88, 68, 0, 87, 88, 88, 88, 87, 81, 88, 81, 86, 88, 0, 88, 87, 88, 88, 1, 82, 241599510, 78, 1, 81, 895514048, 1, 82, 329764124, 78, 0, 53, 268, 3267158399, 66, 88, 269, 54, 24, 88, 53, 88, 88, 91749180, 61, 24, 88, 88, 88, 88, 54, 88, 88, 69, 87, 827490508, 31, 22, 87, 87, 24, 55, 88, 64, 55, 88, 32, 87, 88, 88, 88, 88, 88, 88, 1563209287, 87, 85, 65, 87, 87, 88, 270, 85, 87, 85, 87, 276539820, 1, 81, 1134474295, 1, 86, 2300447074, 66, 88, 271, 86, 86, 87, 81, 88, 87, 2840142209, 67, 87, 85, 87, 2, 86, 87, 87, 87, 86, 87, 88, 88, 88, 88, 68, 2, 87, 87, 87, 87, 87, 81, 88, 81, 86, 87, 0, 87, 88, 87, 88, 1, 82, 3865096360, 78, 1, 81, 3917118647, 1, 82, 3175188937, 78, 0, 56, 272, 197361032, 68, 2, 88, 88, 87, 87, 87, 24, 87, 57, 56, 87, 1, 88, 88, 87, 88, 61, 24, 88, 87, 88, 87, 57, 87, 87, 69, 87, 3487370438, 89, 22, 87, 87, 24, 58, 88, 64, 58, 88, 41, 87, 88, 88, 88, 87, 88, 87, 3496426906, 88, 85, 65, 88, 87, 88, 273, 85, 88, 85, 88, 195121358, 62, 85, 85, 87, 88, 87, 87, 87, 1, 81, 708551016, 1, 86, 2439440605, 66, 87, 274, 86, 86, 88, 81, 88, 87, 3669442709, 70, 1526, 7991, 88, 89, 86, 3433226922, 85, 87, 88, 87, 87, 86, 275, 68, 0, 87, 87, 87, 87, 87, 81, 87, 81, 86, 87, 2, 88, 88, 88, 88, 1, 82, 3645831975, 78, 1, 81, 55910658, 1, 82, 3058427512, 78, 0, 59, 276, 2771915346, 66, 87, 277, 60, 24, 88, 59, 88, 88, 473833898, 61, 24, 88, 88, 88, 87, 60, 88, 88, 69, 88, 1328963825, 108, 22, 87, 88, 24, 61, 88, 64, 61, 88, 20, 88, 87, 87, 88, 88, 88, 88, 67257012, 88, 85, 65, 87, 88, 88, 278, 85, 88, 85, 88, 4068070399, 1, 81, 2941995076, 1, 86, 1072518473, 66, 87, 279, 86, 86, 88, 81, 88, 87, 3402790235, 67, 88, 85, 87, 1, 86, 87, 87, 87, 86, 87, 87, 87, 87, 88, 68, 2, 87, 87, 88, 88, 88, 81, 88, 81, 86, 87, 2, 87, 87, 87, 87, 1, 82, 2397667263, 78, 1, 81, 3061537861, 1, 82, 1127567711, 78, 0, 62, 280, 4105635554, 68, 2, 88, 87, 87, 87, 88, 24, 88, 63, 62, 88, 2, 87, 87, 87, 88, 61, 24, 87, 88, 88, 87, 63, 88, 88, 69, 88, 2361736016, 80, 22, 87, 88, 24, 64, 87, 64, 64, 88, 63, 88, 87, 88, 87, 87, 88, 88, 3423647735, 88, 85, 65, 87, 87, 87, 281, 85, 87, 85, 88, 443934374, 62, 85, 85, 87, 88, 87, 87, 87, 1, 81, 3256105123, 1, 86, 763515870, 66, 87, 282, 86, 86, 87, 81, 87, 87, 1282136846, 70, 36108, 8237, 87, 89, 86, 394823791, 85, 88, 87, 88, 87, 86, 283, 68, 2, 87, 87, 87, 88, 88, 81, 87, 81, 86, 87, 2, 87, 88, 87, 87, 1, 82, 1865871908, 78, 1, 81, 3198257741, 1, 82, 3249455136, 78, 0, 65, 284, 589168952, 66, 88, 285, 66, 24, 88, 65, 88, 88, 1440706506, 61, 24, 87, 87, 87, 87, 66, 87, 88, 69, 87, 3486481077, 16, 22, 88, 88, 24, 67, 88, 64, 67, 88, 36, 88, 88, 87, 87, 88, 88, 88, 3910425401, 87, 85, 65, 88, 87, 87, 286, 85, 88, 85, 88, 3378568280, 1, 81, 3273872253, 1, 86, 622046948, 66, 87, 287, 86, 86, 87, 81, 87, 87, 1847486406, 67, 88, 85, 87, 1, 86, 87, 87, 87, 86, 88, 87, 88, 88, 87, 68, 1, 87, 88, 87, 87, 87, 81, 88, 81, 86, 88, 2, 87, 87, 87, 87, 1, 82, 511988258, 78, 1, 81, 2095300155, 1, 82, 2902211828, 78, 0, 68, 288, 673841586, 66, 87, 289, 69, 24, 87, 68, 87, 88, 2217722074, 61, 24, 87, 88, 87, 87, 69, 88, 88, 69, 88, 2628385510, 73, 22, 87, 88, 24, 70, 88, 64, 70, 88, 37, 87, 87, 87, 87, 87, 87, 88, 3830473707, 87, 85, 65, 87, 87, 87, 290, 85, 87, 85, 88, 40366708, 62, 85, 85, 88, 87, 87, 87, 87, 1, 81, 3050967701, 1, 86, 1249116054, 66, 87, 291, 86, 86, 88, 81, 87, 88, 3005946397, 70, 17883, 8476, 87, 89, 85, 3332046925, 86, 87, 87, 88, 87, 86, 292, 68, 1, 87, 88, 88, 87, 87, 81, 88, 81, 86, 87, 0, 88, 87, 87, 88, 1, 82, 3265120685, 78, 1, 81, 1238498786, 1, 82, 195052464, 78, 0, 71, 293, 2408279214, 68, 0, 88, 87, 88, 88, 88, 24, 87, 72, 71, 88, 0, 87, 88, 87, 88, 61, 24, 87, 87, 87, 87, 72, 88, 88, 69, 88, 2388876876, 79, 22, 87, 87, 24, 73, 87, 64, 73, 87, 80, 87, 87, 88, 87, 87, 88, 87, 2361736016, 88, 85, 65, 87, 88, 88, 294, 85, 87, 85, 88, 4294266926, 1, 81, 1965635379, 1, 86, 343643986, 66, 88, 295, 86, 86, 88, 81, 88, 88, 1504185677, 67, 88, 86, 88, 0, 86, 88, 87, 87, 85, 87, 87, 87, 88, 88, 68, 2, 88, 87, 87, 88, 88, 81, 88, 81, 86, 87, 2, 88, 88, 88, 87, 1, 82, 2239278522, 78, 1, 81, 3397051939, 1, 82, 611707965, 78, 0, 74, 296, 497710610, 66, 88, 297, 75, 24, 88, 74, 88, 88, 792096439, 61, 24, 88, 87, 88, 88, 75, 88, 87, 69, 88, 1117680260, 47, 22, 87, 88, 24, 76, 87, 64, 76, 88, 79, 88, 87, 88, 88, 88, 87, 87, 2388876876, 87, 85, 65, 88, 88, 88, 298, 85, 88, 85, 88, 3832700346, 62, 85, 85, 88, 87, 87, 88, 88, 1, 81, 2753102508, 1, 86, 394732971, 66, 88, 299, 86, 86, 87, 81, 87, 87, 3572075344, 70, 6646, 8722, 87, 89, 86, 2123545599, 85, 88, 87, 87, 88, 86, 300, 68, 2, 87, 88, 88, 87, 88, 81, 87, 81, 86, 88, 1, 88, 87, 88, 87, 1, 82, 2995627984, 78, 1, 81, 4208376299, 1, 82, 1110808927, 78, 0, 77, 301, 1842192103, 66, 87, 302, 78, 24, 87, 77, 87, 88, 1059223469, 61, 24, 88, 87, 87, 87, 78, 88, 88, 69, 87, 884690417, 25, 22, 87, 87, 24, 79, 88, 64, 79, 87, 79, 88, 87, 87, 87, 87, 87, 87, 2388876876, 88, 85, 65, 87, 87, 88, 303, 85, 88, 85, 87, 1915463816, 1, 81, 57705453, 1, 86, 2475321582, 66, 88, 304, 86, 86, 88, 81, 88, 87, 1436326584, 67, 88, 85, 88, 1, 86, 87, 87, 88, 86, 88, 88, 88, 87, 87, 68, 2, 88, 88, 87, 88, 88, 81, 88, 81, 86, 87, 2, 88, 87, 87, 87, 1, 82, 89624959, 78, 1, 81, 1890180395, 1, 82, 3824370806, 78, 1, 81, 2645257952, 1, 82, 2497155311, 78, 1, 80, 9284, 66, 88, 305, 22, 22, 88, 80, 88, 88, 311726219, 1, 81, 185846362, 1, 82, 2777170973, 78, 0, 91, 215, 3003232955, 8, 15, 14, 91, 0, 16, 206, 95854227, 1, 80, 26193, 68, 2, 88, 87, 87, 87, 87, 22, 88, 22, 80, 87, 0, 87, 87, 87, 87, 1, 81, 2805940694, 1, 82, 1274276969, 78, 61, 6, 88, 87, 87, 88, 18, 87, 87, 1, 80, 34068, 68, 1, 87, 88, 88, 88, 88, 22, 87, 22, 80, 88, 1, 88, 87, 87, 87, 1, 81, 1318433020, 1, 82, 1263769051, 78, 74, 88, 17, 8, 87, 88, 87, 87, 87, 88, 91, 87, 88, 87, 87, 43, 92, 14, 15, 1, 91, 68, 1, 88, 88, 88, 88, 87, 6, 88, 18, 92, 88, 0, 88, 88, 87, 87, 1, 80, 28749, 68, 0, 87, 88, 88, 87, 87, 22, 88, 22, 80, 87, 1, 88, 87, 87, 88, 1, 81, 1898257959, 1, 82, 2750526094, 78, 4, 5, 0, 91, 220, 587094060, 61, 6, 87, 87, 87, 87, 91, 87, 87, 1, 80, 14597, 68, 0, 88, 87, 87, 87, 87, 22, 87, 22, 80, 87, 0, 88, 87, 87, 87, 1, 81, 2759623561, 1, 82, 2543448068, 78, 70, 1648, 9089, 88, 89, 12, 3162269649, 13, 87, 87, 87, 88, 91, 306, 61, 8, 88, 88, 88, 87, 91, 88, 87, 2, 14, 26, 3167482679, 1, 80, 6861, 68, 0, 87, 88, 88, 87, 87, 22, 88, 22, 80, 88, 0, 88, 87, 88, 88, 1, 81, 1673969133, 1, 82, 1215546456, 78, 0, 91, 7, 0, 45, 91, 61, 91, 88, 87, 88, 87, 7, 87, 87, 0, 91, 5, 3711018866, 68, 0, 88, 88, 88, 88, 88, 7, 87, 19, 91, 88, 2, 87, 88, 87, 88, 1, 80, 42712, 66, 88, 307, 22, 22, 88, 80, 87, 88, 1702889385, 1, 81, 1743707439, 1, 82, 2226522486, 78, 1, 80, 44109, 71, 3430, 88, 80, 22, 87, 87, 88, 80, 64, 80, 88, 108, 88, 88, 87, 87, 87, 88, 87, 1328963825, 87, 85, 65, 87, 87, 88, 308, 85, 87, 85, 88, 1030592383, 1, 81, 190035692, 1, 86, 4007835824, 66, 87, 309, 86, 86, 87, 81, 88, 87, 3465831811, 67, 88, 85, 88, 1, 86, 88, 88, 88, 86, 88, 88, 88, 88, 88, 68, 1, 88, 88, 87, 87, 88, 81, 87, 81, 86, 87, 1, 87, 88, 87, 88, 1, 82, 1691330938, 78, 1, 80, 37257, 66, 87, 310, 22, 22, 88, 80, 87, 88, 5452089, 1, 81, 2009235614, 1, 82, 3407243665, 78, 1, 80, 7571, 66, 88, 311, 22, 22, 87, 80, 87, 87, 4147162846, 1, 81, 2668333197, 1, 82, 2868571576, 78, 75, 52874, 87, 88, 91, 11, 88, 1, 88, 74, 87, 1, 91, 88, 88, 87, 87, 87, 87, 12, 87, 87, 87, 88, 0, 13, 312, 3945644097, 1, 80, 7032, 66, 87, 313, 22, 22, 88, 80, 88, 88, 444888866, 1, 81, 2426305160, 1, 82, 155027127, 78, 45, 6, 71, 37427, 88, 91, 7, 88, 88, 88, 3, 64, 91, 87, 63, 87, 88, 88, 88, 87, 88, 88, 3423647735, 87, 85, 65, 88, 88, 88, 314, 85, 87, 85, 87, 3101902703, 1, 81, 1394719205, 1, 86, 917545897, 66, 87, 315, 86, 86, 87, 81, 87, 88, 3997051049, 67, 88, 85, 88, 1, 86, 87, 87, 87, 86, 88, 87, 88, 88, 87, 68, 1, 88, 88, 88, 88, 88, 81, 88, 81, 86, 87, 0, 88, 88, 87, 87, 1, 82, 2858294192, 78, 1, 80, 2246, 66, 87, 316, 22, 22, 87, 80, 87, 88, 2432178514, 1, 81, 282090428, 1, 82, 797092251, 78, 1, 80, 6738, 68, 2, 87, 88, 87, 87, 87, 22, 88, 22, 80, 87, 0, 88, 88, 88, 88, 1, 81, 15486051, 1, 82, 1343437466, 78, 1, 80, 52203, 66, 87, 317, 22, 22, 87, 80, 87, 87, 358731773, 1, 81, 1459260842, 1, 82, 506274285, 78, 8, 91, 0, 16, 68, 1, 88, 88, 88, 87, 87, 2, 87, 92, 7, 87, 1, 88, 88, 88, 88, 43, 17, 0, 91, 1, 92, 1, 80, 32117, 66, 88, 318, 22, 22, 87, 80, 87, 87, 973439372, 1, 81, 2434449725, 1, 82, 2380260584, 78, 0, 91, 319, 500343742, 68, 1, 87, 87, 87, 87, 87, 1, 87, 9, 91, 88, 2, 87, 87, 87, 87, 0, 10, 9, 420988975, 1, 80, 43764, 68, 0, 87, 87, 87, 88, 87, 22, 88, 22, 80, 87, 0, 87, 87, 87, 88, 1, 81, 2220785540, 1, 82, 1622341379, 78, 61, 7, 88, 88, 88, 88, 19, 88, 88, 1, 80, 9800, 66, 88, 320, 22, 22, 87, 80, 88, 87, 2739208001, 1, 81, 2349115120, 1, 82, 1653153599, 78, 73, 91, 87, 87, 90, 9, 321, 10, 2293093584, 87, 87, 1732131770, 9658, 87, 322, 61, 1, 87, 87, 87, 87, 91, 87, 88, 0, 11, 323, 1216208883, 1, 80, 54712, 66, 88, 324, 22, 22, 87, 80, 87, 87, 2198439367, 1, 81, 1816656736, 1, 82, 3062422127, 78, 0, 91, 9, 420988975, 61, 7, 87, 88, 88, 88, 91, 88, 88, 1, 80, 7295, 66, 88, 325, 22, 22, 88, 80, 88, 87, 4072083156, 1, 81, 3689636107, 1, 82, 2529031266, 78, 42, 81, 83, 2, 82, 81, 1, 84, 0, 8, 81, 81, 84, 114, 0, 7, 326, 14025992, 0, 8, 9, 420988975, 0, 4, 327, 1986677794, 0, 5, 328, 2635269958, 0, 6, 329, 2041308304, 4, 3, 0, 9, 330, 1112637320, 75, 29930, 6, 5, 10, 9, 6, 1, 6, 74, 6, 1, 10, 5, 6, 4, 6, 6, 6, 9, 6, 6, 6, 5, 0, 10, 9, 420988975, 75, 38251, 6, 5, 11, 10, 5, 9, 5, 61, 1, 4, 4, 6, 4, 11, 6, 6, 68, 0, 6, 5, 6, 5, 4, 1, 4, 9, 0, 5, 1, 6, 6, 5, 6, 0, 10, 9, 420988975, 75, 19740, 4, 5, 11, 10, 5, 9, 5, 61, 1, 6, 6, 5, 6, 11, 5, 5, 0, 9, 331, 2403265848, 74, 6, 1, 9, 6, 4, 6, 4, 6, 6, 10, 6, 6, 6, 6, 0, 9, 9, 420988975, 75, 44120, 5, 6, 11, 9, 5, 10, 5, 61, 1, 6, 6, 5, 6, 11, 4, 5, 0, 9, 332, 3146170889, 72, 6, 5, 5, 4, 5, 5, 1, 5, 6, 10, 9, 4, 6, 0, 9, 333, 1853817276, 75, 18665, 5, 6, 11, 9, 6, 1, 6, 73, 9, 4, 5, 8, 10, 334, 11, 757219706, 5, 5, 3536815826, 9936, 6, 335, 0, 10, 9, 420988975, 75, 9559, 6, 6, 11, 10, 5, 9, 4, 61, 1, 5, 4, 4, 4, 11, 6, 5, 2, 9, 41, 3496426906, 0, 10, 336, 2412827879, 8, 11, 9, 10, 0, 10, 337, 1443538403, 43, 12, 9, 11, 2, 1, 10, 0, 9, 9, 420988975, 75, 9825, 4, 6, 10, 9, 6, 12, 4, 61, 1, 6, 6, 6, 6, 10, 4, 5, 45, 1, 0, 9, 7, 0, 45, 9, 0, 6, 338, 492524022, 0, 9, 339, 2818115212, 0, 10, 9, 420988975, 0, 7, 340, 233535753, 0, 8, 341, 3821361244, 4, 4, 0, 11, 342, 1828975823, 72, 8, 8, 7, 8, 8, 8, 2, 8, 8, 12, 11, 6, 6, 73, 11, 7, 7, 10, 1, 343, 12, 694392318, 8, 7, 4127016290, 10066, 6, 344, 0, 12, 9, 420988975, 75, 9692, 8, 6, 13, 12, 6, 11, 8, 61, 5, 6, 8, 7, 6, 13, 7, 7, 0, 11, 332, 3146170889, 75, 37673, 8, 6, 12, 11, 6, 0, 7, 74, 6, 0, 12, 8, 7, 6, 6, 8, 6, 11, 8, 6, 6, 8, 0, 12, 9, 420988975, 75, 34575, 6, 8, 13, 12, 6, 11, 8, 61, 0, 6, 7, 8, 6, 13, 6, 6, 76, 7, 6, 6, 0, 6, 6, 8, 8, 7, 6, 8, 8, 11, 0, 12, 9, 420988975, 75, 4381, 6, 8, 13, 12, 7, 11, 6, 61, 0, 7, 8, 8, 8, 13, 8, 8, 76, 6, 6, 6, 0, 6, 8, 7, 7, 6, 8, 8, 8, 11, 0, 12, 9, 420988975, 75, 2149, 8, 7, 13, 12, 7, 11, 6, 61, 0, 6, 8, 6, 6, 13, 8, 7, 68, 1, 7, 6, 7, 7, 7, 0, 6, 11, 5, 8, 1, 7, 8, 8, 8, 0, 12, 9, 420988975, 75, 40803, 8, 6, 13, 12, 7, 11, 8, 61, 0, 7, 7, 7, 7, 13, 8, 8, 2, 11, 41, 3496426906, 0, 12, 336, 2412827879, 8, 13, 11, 12, 0, 12, 345, 514057217, 43, 14, 11, 13, 2, 0, 12, 0, 11, 9, 420988975, 75, 58318, 6, 8, 12, 11, 6, 14, 8, 61, 0, 6, 7, 8, 7, 12, 7, 7, 0, 11, 222, 114532842, 49, 12, 1, 11, 0, 45, 12, 0, 11, 7, 0, 45, 11, 0, 6, 346, 2241369911, 0, 4, 347, 2307576799, 0, 5, 348, 3722941397, 4, 3, 0, 7, 349, 3083672463, 74, 5, 1, 7, 4, 5, 6, 4, 4, 6, 8, 4, 6, 5, 5, 0, 7, 9, 420988975, 75, 60864, 5, 6, 9, 7, 6, 8, 6, 61, 1, 5, 6, 4, 4, 9, 4, 6, 2, 7, 41, 3496426906, 0, 8, 336, 2412827879, 8, 9, 7, 8, 0, 8, 350, 4012242881, 43, 10, 7, 9, 2, 1, 8, 0, 7, 9, 420988975, 75, 59040, 4, 5, 8, 7, 6, 10, 5, 61, 1, 5, 6, 5, 5, 8, 6, 6, 74, 5, 1, 0, 4, 6, 4, 5, 5, 5, 7, 5, 4, 5, 4, 0, 8, 9, 420988975, 75, 37453, 5, 4, 9, 8, 5, 7, 5, 61, 1, 5, 5, 5, 5, 9, 5, 6, 0, 7, 342, 1828975823, 75, 14903, 5, 5, 8, 7, 4, 1, 5, 74, 4, 1, 8, 5, 5, 5, 6, 4, 4, 7, 5, 5, 5, 5, 0, 8, 9, 420988975, 75, 48190, 6, 4, 9, 8, 4, 7, 5, 61, 1, 4, 6, 6, 4, 9, 6, 6, 48, 7, 1, 1, 45, 7, 0, 7, 7, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 20; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 77: + { + // JUMP_3449 (specialized) + regs[fp + SLOTS.PC] = 3449; + break; + } + case 78: + { + // JUMP_9724 (specialized) + regs[fp + SLOTS.PC] = 9724; + break; + } + case 79: + { + // JUMP_6521 (specialized) + regs[fp + SLOTS.PC] = 6521; + break; + } + case 80: + { + // LOAD_INT_18_8 (specialized) + var dst = 18; + regs[base + dst] = 8; + break; + } + case 81: + { + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 82: + { + // LOAD_CONST_13_0_3895797497 (specialized) + var dst = 13; + regs[base + dst] = this._constant(0, 3895797497); + break; + } + case 83: + { + // LOAD_CONST_14_1_3155301703 (specialized) + var dst = 14; + regs[base + dst] = this._constant(1, 3155301703); + break; + } + case 84: + { + // LOAD_CONST_12_2_1921576715 (specialized) + var dst = 12; + regs[base + dst] = this._constant(2, 1921576715); + break; + } + case 85: + { + // LOAD_CONST_7_3_893962770 (specialized) + var dst = 7; + regs[base + dst] = this._constant(3, 893962770); + break; + } + case 86: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 87: + { + // LOAD_GLOBAL_15_4_3173409969 (specialized) + var dst = 15; + var globalName = this._constant(4, 3173409969); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 88: + { + // LOAD_INT_9_2169651486 (specialized) + var dst = 9; + regs[base + dst] = 2169651486; + break; + } + case 89: + { + // LOAD_INT_10_228 (specialized) + var dst = 10; + regs[base + dst] = 228; + break; + } + case 90: + { + // LOAD_INT_11_11 (specialized) + var dst = 11; + regs[base + dst] = 11; + break; + } + case 91: + { + // LOAD_GLOBAL_17_4_3173409969 (specialized) + var dst = 17; + var globalName = this._constant(4, 3173409969); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 92: + { + // GET_PROP_19_17_4 (specialized) + // dst = regs[obj][regs[key]] + var dst = 19; + var obj = regs[base + 17]; + var key = regs[base + 4]; + regs[base + dst] = obj[key]; + break; + } + case 93: + { + // SET_PROP_15_3_17 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 15]; + var key = regs[base + 3]; + var val = regs[base + 17]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 94: + { + // RETURN_15 (specialized) + var retVal = regs[base + 15]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 95: + { + // LOAD_THIS_19 (specialized) + var dst = 19; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 96: + { + // THROW_96 (specialized) + throw regs[base + 96]; + break; + } + case 97: + { + // FINALLY_SETUP_3144_4_5_3445 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 3144, + contReg: 4, + payloadReg: 5, + throwPad: 3445 + }); + break; + } + case 98: + { + // TRY_SETUP_2854_3 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 2854, + exceptionReg: 3 + }); + break; + } + case 99: + { + // GET_PROP_96_3_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 96; + var obj = regs[base + 3]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; + break; + } + case 100: + { + // LOAD_GLOBAL_7_108_1328963825 (specialized) + var dst = 7; + var globalName = this._constant(108, 1328963825); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 101: + { + // LOAD_UPVALUE_14_0 (specialized) + var dst = 14; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 102: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 103: + { + // SET_PROP_11_12_13 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 11]; + var key = regs[base + 12]; + var val = regs[base + 13]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 104: + { + // JUMP_REG_4 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 4]; + break; + } + case 105: + { + // LOAD_GLOBAL_11_4_3173409969 (specialized) + var dst = 11; + var globalName = this._constant(4, 3173409969); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 106: + { + // RETURN_96 (specialized) + var retVal = regs[base + 96]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 107: + { + // THROW_5 (specialized) + throw regs[base + 5]; + break; + } + case 108: + { + // JUMP_REG_86 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 86]; + break; + } + case 109: + { + // LOAD_THIS_15 (specialized) + var dst = 15; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 110: + { + // RETURN_82 (specialized) + var retVal = regs[base + 82]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 111: + { + // GET_PROP_11_3_82 (specialized) + // dst = regs[obj][regs[key]] + var dst = 11; + var obj = regs[base + 3]; + var key = regs[base + 82]; + regs[base + dst] = obj[key]; + break; + } + case 112: + { + // GET_PROP_83_3_82 (specialized) + // dst = regs[obj][regs[key]] + var dst = 83; + var obj = regs[base + 3]; + var key = regs[base + 82]; + regs[base + dst] = obj[key]; + break; + } + case 113: + { + // JUMP_REG_70 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 70]; + break; + } + case 114: + { + // JUMP_REG_81 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 81]; + break; + } + case 61: + { + // ANTI_MOVE_0_5_2_7_6_1_3_4 (order: [0,5,2,7,6,1,3,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[3]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // LTE + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a <= regs[base + _operands[4]]; + // GTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a >= regs[base + _operands[7]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_6_5_0_1_3_2_4 (order: [6,5,0,1,3,2,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[0]]; + // UNARY_POS + var dst = _operands[0]; + regs[base + dst] = +regs[base + _operands[1]]; + // MOD + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a % regs[base + _operands[4]]; + // UNARY_NEG + var dst = _operands[5]; + regs[base + dst] = -regs[base + _operands[6]]; + break; + } + case 63: + { + // ANTI_NEQ_12_13_8_4_9_6_3_11_7_2_0_5_1_10 (order: [12,13,8,4,9,6,3,11,7,2,0,5,1,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[12], _unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[4], _unsortedOperands[13], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[1]]; + // LOAD_CONST + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + // BXOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a ^ regs[base + _operands[5]]; + // LOAD_INT + var dst = _operands[6]; + regs[base + dst] = _operands[7]; + // EXP + // Math.pow instead of `**` + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[10]]); + // NEQ + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a !== regs[base + _operands[13]]; + break; + } + case 64: + { + // ANTI_UNARY_NOT_4_0_6_11_5_9_10_12_2_1_7_8_3 (order: [4,0,6,11,5,9,10,12,2,1,7,8,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[12], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[10], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[7]]; + // GT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a > regs[base + _operands[2]]; + // UNARY_NOT + var dst = _operands[3]; + regs[base + dst] = !regs[base + _operands[4]]; + // LOAD_GLOBAL + var dst = _operands[5]; + var globalName = this._constant(_operands[6], _operands[7]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LT + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a < regs[base + _operands[10]]; + // UNARY_NOT + var dst = _operands[11]; + regs[base + dst] = !regs[base + _operands[12]]; + break; + } + case 65: + { + // ANTI_UNARY_POS_5_6_7_1_4_8_3_0_2 (order: [5,6,7,1,4,8,3,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[5]]; + // LOAD_CONST + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + // UNARY_POS + var dst = _operands[3]; + regs[base + dst] = +regs[base + _operands[4]]; + // UNARY_POS + var dst = _operands[5]; + regs[base + dst] = +regs[base + _operands[6]]; + // TYPEOF + var dst = _operands[7]; + regs[base + dst] = typeof regs[base + _operands[8]]; + break; + } + case 66: + { + // ANTI_SUB_6_7_3_4_0_5_1_2_8 (order: [6,7,3,4,0,5,1,2,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[8]]; + // LOOSE_NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a != regs[base + _operands[2]]; + // SUB + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a - regs[base + _operands[5]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[6]; + var name = this._constant(_operands[7], _operands[8]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + case 67: + { + // ANTI_MUL_4_13_9_6_11_2_8_1_12_0_5_7_3_10 (order: [4,13,9,6,11,2,8,1,12,0,5,7,3,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[12], _unsortedOperands[0], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[11], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[13], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[1]]; + // LTE + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a <= regs[base + _operands[2]]; + // UNARY_NOT + var dst = _operands[3]; + regs[base + dst] = !regs[base + _operands[4]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[5]; + var count = _operands[6]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[7]]; + regs[base + dst] = elems; + // ADD + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a + regs[base + _operands[10]]; + // MUL + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a * regs[base + _operands[13]]; + break; + } + case 68: + { + // ANTI_ADD_8_14_15_6_0_2_11_13_10_12_3_1_4_5_9_7 (order: [8,14,15,6,0,2,11,13,10,12,3,1,4,5,9,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[12], _unsortedOperands[13], _unsortedOperands[3], _unsortedOperands[15], _unsortedOperands[0], _unsortedOperands[14], _unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[2]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[0]; + var pairCount = _operands[1]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[2]]; + var val = regs[base + _operands[3]]; + o[key] = val; + } + regs[base + dst] = o; + // GT + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a > regs[base + _operands[6]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[7]; + var count = _operands[8]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[9]]; + regs[base + dst] = elems; + // ADD + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a + regs[base + _operands[12]]; + // USHR + var dst = _operands[13]; + var a = regs[base + _operands[14]]; + regs[base + dst] = a >>> regs[base + _operands[15]]; + break; + } + case 69: + { + // ANTI_EQ_3_8_7_1_4_5_2_0_6 (order: [3,8,7,1,4,5,2,0,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[2], _unsortedOperands[1]]; + // EQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a === regs[base + _operands[2]]; + // LT + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a < regs[base + _operands[5]]; + // LOAD_GLOBAL + var dst = _operands[6]; + var globalName = this._constant(_operands[7], _operands[8]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 70: + { + // ANTI_BAND_7_9_4_8_11_2_12_5_0_3_6_10_1 (order: [7,9,4,8,11,2,12,5,0,3,6,10,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[12], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[6]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this._constant(_operands[1], _operands[2]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a !== regs[base + _operands[5]]; + // LOAD_INT + var dst = _operands[6]; + regs[base + dst] = _operands[7]; + // JUMP_IF_FALSE + var src = _operands[8]; + var target = _operands[9]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // BAND + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a & regs[base + _operands[12]]; + break; + } + case 71: + { + // ANTI_LT_1_0_5_6_2_4_3_7 (order: [1,0,5,6,2,4,3,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[7]]; + // LOAD_INT + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + // BXOR + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a ^ regs[base + _operands[4]]; + // LT + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a < regs[base + _operands[7]]; + break; + } + case 72: + { + // ANTI_SHL_0_11_10_2_12_8_6_9_1_5_7_3_4 (order: [0,11,10,2,12,8,6,9,1,5,7,3,4]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[4]]; + // ADD + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; + // UNARY_NOT + var dst = _operands[3]; + regs[base + dst] = !regs[base + _operands[4]]; + // SHL + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a << regs[base + _operands[7]]; + // BXOR + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a ^ regs[base + _operands[10]]; + // MOVE + var dst = _operands[11]; + regs[base + dst] = regs[base + _operands[12]]; + break; + } + case 73: + { + // ANTI_BOR_6_9_0_12_7_4_8_5_1_2_11_13_3_10 (order: [6,9,0,12,7,4,8,5,1,2,11,13,3,10]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[12], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[13], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[11]]; + // SUB + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a - regs[base + _operands[2]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[3]; + var name = this._constant(_operands[4], _operands[5]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // BOR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a | regs[base + _operands[8]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[9]; + var name = this._constant(_operands[10], _operands[11]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // JUMP_IF_TRUE + // || short-circuit: if truthy, jump over RHS. + var src = _operands[12]; + var target = _operands[13]; + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case 74: + { + // ANTI_BXOR_3_6_7_11_10_4_8_12_0_5_9_1_13_2 (order: [3,6,7,11,10,4,8,12,0,5,9,1,13,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[11], _unsortedOperands[13], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[12]]; + // TYPEOF + var dst = _operands[0]; + regs[base + dst] = typeof regs[base + _operands[1]]; + // DIV + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a / regs[base + _operands[4]]; + // BXOR + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a ^ regs[base + _operands[7]]; + // USHR + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a >>> regs[base + _operands[10]]; + // ADD + var dst = _operands[11]; + var a = regs[base + _operands[12]]; + regs[base + dst] = a + regs[base + _operands[13]]; + break; + } + case 75: + { + // ANTI_USHR_1_5_7_2_4_0_3_6 (order: [1,5,7,2,4,0,3,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[2]]; + // LOAD_INT + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + // USHR + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a >>> regs[base + _operands[4]]; + // MOD + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a % regs[base + _operands[7]]; + break; + } + case 76: + { + // ANTI_UNARY_BITNOT_7_2_9_12_3_10_4_0_5_8_1_6_11 (order: [7,2,9,12,3,10,4,0,5,8,1,6,11]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[11], _unsortedOperands[0], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[12], _unsortedOperands[3]]; + // SHR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a >> regs[base + _operands[2]]; + // UNARY_NEG + var dst = _operands[3]; + regs[base + dst] = -regs[base + _operands[4]]; + // LTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a <= regs[base + _operands[7]]; + // EQ + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a === regs[base + _operands[10]]; + // UNARY_BITNOT + var dst = _operands[11]; + regs[base + dst] = ~regs[base + _operands[12]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/qualification.json new file mode 100644 index 00000000..5505c568 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/qualification.json @@ -0,0 +1,77 @@ +{ + "id": "exceptions--stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodes--88", + "case": "exceptions", + "profile": "stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodes", + "seed": 88, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true, + "concealConstants": true, + "antiInstrumentation": true, + "specializedOpcodes": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-anti-specialized-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/encoded.js new file mode 100644 index 00000000..8e4e8a91 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/encoded.js @@ -0,0 +1,1809 @@ +var CONSTANTS = [/* 0 */"YbBbGD6A3wcikLQoFqCH2WlQbPgUQHPInTAmqL3hMXiTEHyYtwMriIAQ9fnnYYfYBjCVS1jAukhc8IVZ76DJONOT9g/tgM8Y5ZHLOaugH2jD/7NI68DJud9R09jlYNXv/xO+uAUBg5km8Ypo/B+Qq9Qw79izQTv5nFA3L6ejFyeLkBZ5iYFrCRu/qSsuZ6X4D1GU2Q9BvUhY02h3TeMOmVgxR4l5AXuLzXdom2Yharlr0TpJfMN2RyTTTD8TocLpFHGlCiqHazt8j3kZeOEFaYnSLjehQxDfvnIS6elhJam7Noyb/A+Geh4RnKkiIaOmGVPX//FCmcaIscwpxubZeuzvuRvVhtwJuJH09u9i5N+Is8c2+cLcSTz2zVojLqW7DRbCipgB6Yn5EjC+jiMsZrDyCU6UwXA6805i2mlmXOoonr4JF4K2Hk7y0XcP4qUuZ7Id2mhOTfpQVmyqYD4/qrQRTn5kAlqGQjJUrnpiS+ZcblfaGEa4Swvem2oN5mEeY7IVNnKCCg6YciIWyK0cOqPWIEqJ/2FKw9bVvQgihOZlcoePPQLQhjgyr53o5ppq7t+KS+hG2drpTQr252LKL86z0hamgux92ZX8Kty+oCtdV5j6KU3LxWo1/q7a4+hn2fI+neIFHIXDHnbromcx25wuCbVVxXNNP/OKV12jmz4KlaMVFn1XC0WXATtur0dlY/VQTTzdHkdlM3bfR2Uu5WodZo0GB1ebfw+4dFPly60UPabUYkNA/1BTBNWVvR8tvJQve5WPbhv0tAYtgp055eV0xt8QQ6ZERd22bSPljGQrL/G72QT9jPx90pWnJM+8wNMoV+D8003T1eM0/KzwG/533gz9nIEFIrSLHDXssWeQ3OtM0UX0xaJMPPyvZySszTw4lIsFEHyLDFGUeDz/rCpUpvUlTJ/cAbSjK1rccWQR9EscZYxABHeUf+xFZBHkSqxoPEbES0QD/F9UPiRovH4sq5Q3BJKPMRS2tGgsHVxN9AV0td8RRPu0Ktz3bQXkuJQDDKi77QTyjL19L+S9JD28ydMARNP81U3i1dYk+qzUG9l38wzqnLM18LTzHPvj43fY3PxM4kXz1f5sLfuqZ0+rkTwrhe4F/3zZ7ISn6iO5rCZV48UZTY7cKbe1KxafknQU9VMd4ow7N6CTXu+5ZFTVjU1rPXzHQ0tk/1JrcyV0vV0Nq4RkA0+PMBNcpW4tDV1Y9SNLWN8GS4O2I92NbQP1bZMeD3eDeRdk7a59MeW6Ki2/+8sIR6n6HW3gpYoi3a75EzSHmgIrndE1AqXVXtPr5nfp2vdO6zXH1cRuyuLLF7OStz7VhfoV3H7h6vmn1zvBzgJaqMVQTo/SBabRI5mekHL1hRIehYoqNvGbNO6leiHWvU14QkDWclII/qdiNRabvVYK3oYIenHuZhpCpg4qQ1JHxmlCQt5kMmmmdiJ4akf2Xpp9DkG6aBY26lN+X9a+UlS+kcMbVnPyBm5Kpkc6OI7wGy6GnwoQnrEpMqKoXiPy7kfdwjxOlDEy2t5uS/qxFx6K2T7kieAR+X7d4uBX3zPMzhhCy/n7bq/a9KfHK+6eiQrQgbMZr7IWJ8yjOO/Wcg7Z3UH0uobW5msF/6xqIhGfuSUB8ZYoc/DvMSMNuRwxOFmHxjBLld8sK5amHxlhcXj5eINaD2SzUSdV4T55JNFvS28/WctSV3P5CmFUmWIxbY/HAxx3qvBZmYAhZKlcXw7LRkeh2Cux+zk7oatoCfP0BySDgDjEkeDp22A565BXITvIwAdZ0PHaaOog/LeIM9KY5ADwiaARz6gaJ9Nb5fD+eOLB2UnHsMQowmOh/6KQGgjVgTEIv5D8e83v+lj9sJPJbkC9yCFwh98HIPWoMxDgiVkAVJiYLxO4riAH6L9xEMiVQFs4QNBZaFjgcWhzkHM4ZIBjGEBwXeh6oD4oOLANWHbAIUh30AOoiCExmIQQTfi5B2yIdTALqEbgpXgI0IZXBcCLyHJQ6vkhYKAY97fLC6KAHAiF8QMYr6BPOI9TrciYQOfZ0FH4uKYg5Wvm/5SI0gGzucoQ3pjCY+vvxlD9uPbBB1mz8DBr1t8hK5Gw3xnVgfn4FJOULw6j4Vgo0Zd5GMGxSAE/bKtYZ2X5nxFpmbMAII+VE1t3p+BWWW8xdcl+X0rLL3c621ihjjk1kTUPeYN9B12TfTlioSLpU3EXy5JHBLuJzmpZfuF/aX4TAbc3A7KeJnGf6Slh8aowN5ZbPc/seoDRxinlogkn2JMv/5eCUgnxseYqm+b9e8Pv9FqoxtHJ5lHq9uRjwJ/vMv62gAGJicj21JrzLzm6qlYMyqhBLsbMUtk/16P1ZiTyjwkfhqM6Oo7gGyVmsOo8Xkw2BMIRXrzzEHaM4iqujzEmmlMOfPpHFgeqQj47qizSIH5LwmVGbbJ/LmJiaPoMbkAaeYZaCoeeEWq05QlejrI1Nl9CUs5acvvVGC6KuiAWhYsR/piKFhaCvSQix+bZczLO50I6xqrNFVrT5lBq+p/dCuOGyR3Njdj2oWL9Lwyy11bmzcZ959bHquQ/hqvCFt36vQ0UhZMyOK+c42xnwfKHXcDFvcoVX5LrOmeyGgGthzU4hbkOX/NGF0ajIj2VVSfVtX4Ay6hXSLssLWtlevVUjVUDX4dQM22fM2Vk5S9dV8ttx1Bbfs9edVrlQy2cvCgXmIMo/5aUQxUcvQ0tWdeTewnPg0Q+NbKtJF2v5BNzj5/xBNKE1B1rbZ/kJ1uyLzW7yETazcB9gtSLpNU/o5OgFPn83Q3ulP8036/P27109czBXenF+zQOXNnjKmSrHDWMuwX8hMwM3/uZZ+CsHjyCVSnE9HwizIxXALz4LEeUU/SejD8cvbcDLI6cV2SS9XhcYcx5xENcSuxYZFDkaiwzvGIEOowo/F+UbiRtvAIsVtR6RB7MjVQUNIutIxzp9Gl0Z5xVBDm06B0SbIjkbVSdzzHErkQbzOV90eT2pII/FJSHBC78kQ0UlNUkzq/J29J0+MzuXcm15TS4XMjj+3SrHO+NzQWElOtsh/vYZ8qsVj2SRTnFtXwV0+knxLydLOSVOgW/jAwbiLcNK4DcfHWw9T1dQsMBt2lTpuwLZbyVNA2GvEIHQItS/0FleSVdvWZNU8dtQzzPN1VKJU2tXR1B90ILR58YCy21UB0gbdf2K1OizyrDYVV37U19ROUZq5E/KYu/Bi39mW0HpaIiCK/G09NGwcLiXRa1qCoOn7HrFHf0GsaN5DXfksZu7+PwV9rSwT7YRbjKy37o694nirriDpGF", /* 1 */"CXuIDwqbezVFrUJd", /* 2 */-1225681961, /* 3 */-1027593899, /* 4 */466196371, /* 5 */-815039144, /* 6 */undefined, /* 7 */163957657, /* 8 */-364940579, /* 9 */1342876386, /* 10 */-1596598937, /* 11 */36885457, /* 12 */715912967, /* 13 */-1976774036, /* 14 */-953517721, /* 15 */241602776, /* 16 */-1098771437, /* 17 */-1315987051, /* 18 */329760290, /* 19 */-429876662, /* 20 */-2006344887, /* 21 */-1578504602, /* 22 */-1119809211, /* 23 */-649161566, /* 24 */1832383895, /* 25 */-1557640537, /* 26 */-1236536228, /* 27 */-1897300041, /* 28 */-1664035651, /* 29 */"EhOdvg8F6YXnfs7qxSfBu+ouKlU=", /* 30 */1127567710, /* 31 */"wgDcuMsRyZnXYAzo", /* 32 */"IbK3LSjmqno=", /* 33 */"BCaeugsujVsNxvhK", /* 34 */"jsYNQ43PDHPf5sIt1r3OFvKK+f/wi+oW", /* 35 */"", /* 36 */-259122619, /* 37 */-330439415, /* 38 */-1392726089, /* 39 */-1029848156, /* 40 */1624358640, /* 41 */982469550, /* 42 */195048726, /* 43 */-2055691934, /* 44 */-1312584561, /* 45 */1249522895, /* 46 */611663822, /* 47 */-1299364806, /* 48 */589866305, /* 49 */-53607920, /* 50 */1110822082, /* 51 */89634060, /* 52 */-218233556, /* 53 */-233792262, /* 54 */-470610265, /* 55 */-1797825278, /* 56 */-1517765662, /* 57 */1274276964, /* 58 */-714806174, /* 59 */-1544441186, /* 60 */"e8dQQ1nPPlU=", /* 61 */"IQuykCcp3aE=", /* 62 */-1597897231, /* 63 */-415287816, /* 64 */-608290170, /* 65 */-991088222, /* 66 */876099883, /* 67 */-1426395732, /* 68 */1502079524, /* 69 */-1436673114, /* 70 */-2105085417]; +var BYTECODE = [47, 4, 55, 1, 81, 0, 0, 85, 42, 3, 4, 1, 5, 47, 6, 1284, 4, 89, 0, 0, 86, 87, 88, 89, 90, 42, 2, 6, 4, 3, 7, 8, 9, 91, 92, 13, 13, 11, 12, 93, 11, 12, 13, 11, 0, 11, 5, 3479928157, 94, 95, 0, 10, 6, 0, 96, 47, 76, 2575, 3, 10, 0, 0, 97, 0, 16, 7, 163950657, 98, 0, 18, 8, 3930026717, 99, 0, 20, 9, 1342881584, 100, 101, 63, 69, 102, 1, 78, 67877309, 64, 72, 65, 1, 74, 1839, 1, 75, 48152, 62, 0, 22, 10, 2698332972, 103, 104, 105, 63, 1, 73, 1063617810, 1, 78, 4175699419, 64, 78, 65, 1, 74, 36833, 1, 75, 63718, 62, 1, 73, 263276511, 1, 74, 58662, 1, 75, 2331, 62, 0, 24, 11, 36883500, 11, 25, 19, 24, 106, 107, 108, 63, 69, 1, 73, 239776783, 1, 78, 3351858392, 64, 79, 65, 1, 74, 23251, 1, 75, 15177, 62, 1, 73, 1026632055, 1, 74, 10057, 1, 75, 23043, 62, 0, 27, 12, 715900432, 12, 28, 19, 27, 5, 19, 28, 109, 34, 77, 29, 63, 69, 1, 73, 2222160244, 1, 78, 3761108761, 64, 72, 65, 1, 74, 46967, 1, 75, 18210, 62, 1, 73, 1830248592, 1, 74, 14165, 1, 75, 6017, 62, 0, 30, 13, 2318196109, 11, 31, 19, 30, 5, 19, 31, 110, 34, 77, 32, 63, 1, 73, 3165500492, 1, 78, 1196048259, 64, 80, 65, 1, 74, 37893, 1, 75, 60521, 62, 1, 73, 2263834229, 1, 74, 13358, 1, 75, 4852, 62, 0, 33, 14, 3341439863, 11, 34, 19, 33, 5, 19, 34, 26, 35, 17, 19, 34, 77, 35, 63, 69, 1, 73, 1913771937, 1, 78, 3846003727, 64, 72, 65, 1, 74, 37081, 1, 75, 27796, 62, 1, 73, 3090562720, 1, 74, 26078, 1, 75, 40962, 62, 0, 36, 15, 241599510, 12, 37, 19, 36, 5, 19, 37, 26, 38, 17, 19, 34, 77, 38, 63, 1, 73, 1469536234, 1, 78, 3795051297, 64, 78, 65, 1, 74, 4272, 1, 75, 59556, 62, 1, 73, 2693081334, 1, 74, 43924, 1, 75, 50037, 62, 0, 39, 16, 3196192454, 11, 40, 19, 39, 5, 19, 40, 26, 41, 17, 19, 34, 77, 41, 63, 69, 1, 73, 909496413, 1, 78, 2666732656, 64, 72, 65, 1, 74, 44778, 1, 75, 19980, 62, 1, 73, 447445742, 1, 74, 60111, 1, 75, 61454, 62, 0, 42, 17, 2978973816, 11, 43, 19, 42, 5, 19, 43, 26, 44, 17, 19, 34, 77, 44, 63, 69, 1, 73, 2900350316, 1, 78, 930898083, 64, 72, 65, 1, 74, 46880, 1, 75, 47233, 62, 1, 73, 652775030, 1, 74, 21555, 1, 75, 39441, 62, 0, 45, 18, 329764124, 12, 46, 19, 45, 5, 19, 46, 26, 47, 17, 19, 34, 77, 47, 63, 69, 1, 73, 3232391200, 1, 78, 3054359785, 64, 72, 65, 1, 74, 1621, 1, 75, 63877, 62, 1, 73, 2875769965, 1, 74, 4923, 1, 75, 2449, 62, 0, 48, 19, 3865096360, 12, 49, 19, 48, 5, 19, 49, 26, 50, 17, 19, 34, 77, 50, 63, 69, 1, 73, 2492750251, 1, 78, 3319573108, 64, 72, 65, 1, 74, 6313, 1, 75, 16636, 62, 1, 73, 2292687141, 1, 74, 48068, 1, 75, 64152, 62, 0, 51, 20, 2288618524, 11, 52, 19, 51, 5, 19, 52, 26, 53, 17, 19, 34, 77, 53, 63, 1, 73, 2498806818, 1, 78, 2145779856, 64, 80, 65, 1, 74, 15082, 1, 75, 17358, 62, 1, 73, 1815718929, 1, 74, 28335, 1, 75, 3810, 62, 0, 54, 21, 2716447597, 12, 55, 19, 54, 5, 19, 55, 26, 56, 17, 19, 34, 77, 56, 63, 69, 1, 73, 2473831117, 1, 78, 1396535023, 64, 72, 65, 1, 74, 64816, 1, 75, 59696, 62, 1, 73, 2376721609, 1, 74, 1070, 1, 75, 19758, 62, 0, 57, 22, 3175188937, 11, 58, 19, 57, 5, 19, 58, 26, 59, 17, 19, 34, 77, 59, 63, 1, 73, 3934176499, 1, 78, 3537857358, 64, 80, 65, 1, 74, 29896, 1, 75, 65228, 62, 1, 73, 1469960299, 1, 74, 22245, 1, 75, 27783, 62, 0, 60, 23, 3645831975, 12, 61, 19, 60, 5, 19, 61, 26, 62, 17, 19, 34, 77, 62, 63, 69, 1, 73, 3542054354, 1, 78, 2577456393, 64, 72, 65, 1, 74, 52625, 1, 75, 20014, 62, 1, 73, 2102853634, 1, 74, 56207, 1, 75, 29197, 62, 0, 63, 24, 1832339491, 11, 64, 19, 63, 5, 19, 64, 26, 65, 17, 19, 34, 77, 65, 63, 1, 73, 2974328547, 1, 78, 2621301585, 64, 78, 65, 1, 74, 16272, 1, 75, 41057, 62, 1, 73, 2008534255, 1, 74, 20981, 1, 75, 25012, 62, 0, 66, 25, 2737314274, 12, 67, 19, 66, 5, 19, 67, 26, 68, 17, 19, 34, 77, 68, 63, 69, 1, 73, 2504116106, 1, 78, 752951599, 64, 79, 65, 1, 74, 1557, 1, 75, 13249, 62, 1, 73, 3646301168, 1, 74, 195, 1, 75, 43381, 62, 0, 69, 26, 3058427512, 11, 70, 19, 69, 5, 19, 70, 26, 71, 17, 19, 34, 77, 71, 63, 1, 73, 1775941221, 1, 78, 3883168558, 64, 78, 65, 1, 74, 18023, 1, 75, 47903, 62, 1, 73, 1023539775, 1, 74, 15703, 1, 75, 64967, 62, 1, 73, 2471485824, 1, 74, 17807, 1, 75, 17095, 62, 11, 79, 5, 13, 43, 12, 3, 11, 1, 79, 0, 14, 27, 2397667263, 1, 72, 39052, 73, 1, 73, 4222078970, 1, 74, 20492, 1, 75, 23587, 62, 0, 79, 28, 2630931647, 11, 80, 5, 79, 5, 5, 80, 1, 72, 11474, 74, 1, 73, 3107483811, 1, 74, 21545, 1, 75, 57630, 62, 1, 72, 13931, 73, 1, 73, 3747621707, 1, 74, 53984, 1, 75, 25219, 62, 111, 112, 43, 9, 7, 8, 1, 80, 1, 72, 1722, 74, 1, 73, 1146788126, 1, 74, 25836, 1, 75, 64501, 62, 0, 79, 29, 4029277889, 113, 0, 13, 30, 1127567711, 1, 72, 42394, 74, 1, 73, 1024029931, 1, 74, 15670, 1, 75, 57529, 62, 11, 79, 4, 9, 5, 4, 79, 1, 72, 21737, 73, 1, 73, 1474413734, 1, 74, 57385, 1, 75, 7266, 62, 1, 72, 33841, 83, 84, 63, 1, 73, 4103733298, 1, 78, 2134281065, 64, 80, 65, 1, 74, 5673, 1, 75, 33107, 62, 1, 72, 20996, 74, 1, 73, 546200016, 1, 74, 40829, 1, 75, 54301, 62, 1, 72, 741, 73, 1, 73, 801285754, 1, 74, 31100, 1, 75, 60026, 62, 0, 79, 31, 1865871908, 114, 115, 34, 77, 79, 63, 69, 1, 73, 1689063396, 1, 78, 4014578459, 64, 79, 65, 1, 74, 6767, 1, 75, 26054, 62, 1, 72, 9843, 74, 1, 73, 2088625842, 1, 74, 29427, 1, 75, 12654, 62, 1, 72, 29747, 74, 1, 73, 2447817593, 1, 74, 16406, 1, 75, 24066, 62, 116, 0, 79, 29, 4029277889, 117, 43, 10, 3, 80, 1, 5, 1, 72, 9129, 73, 1, 73, 3086211653, 1, 74, 49355, 1, 75, 51099, 62, 0, 79, 6, 0, 118, 119, 120, 42, 6, 79, 1, 0, 1, 72, 17008, 73, 1, 73, 3820676587, 1, 74, 38955, 1, 75, 31172, 62, 1, 72, 378, 74, 1, 73, 3768733357, 1, 74, 60267, 1, 75, 49913, 62, 1, 72, 34308, 83, 84, 63, 69, 1, 73, 3458633167, 1, 78, 2388445613, 64, 79, 65, 1, 74, 51561, 1, 75, 2594, 62, 1, 72, 30252, 74, 1, 73, 1559528634, 1, 74, 54093, 1, 75, 58560, 62, 1, 72, 0, 73, 1, 73, 1592473838, 1, 74, 62057, 1, 75, 23524, 62, 121, 0, 79, 34, 3249455136, 122, 1, 72, 15419, 73, 1, 73, 919456137, 1, 74, 7459, 1, 75, 4089, 62, 5, 3, 6, 0, 79, 35, 511988258, 5, 4, 79, 1, 72, 8189, 74, 1, 73, 4013948459, 1, 74, 63673, 1, 75, 3885, 62, 0, 79, 8, 3930026717, 5, 5, 79, 1, 72, 9950, 73, 1, 73, 1611022954, 1, 74, 40460, 1, 75, 44230, 62, 42, 73, 76, 3, 73, 74, 75, 123, 47, 84, 2725, 3, 10, 0, 0, 124, 0, 21, 36, 4035882083, 5, 22, 21, 0, 23, 8, 3930026717, 5, 24, 23, 0, 25, 37, 3964479750, 125, 34, 85, 26, 66, 1, 81, 3347564137, 1, 86, 1323652863, 67, 77, 68, 1, 82, 8639, 1, 83, 61695, 61, 0, 27, 38, 2902211828, 5, 24, 27, 26, 28, 22, 24, 34, 85, 28, 66, 1, 81, 1150139250, 1, 86, 1281860604, 67, 81, 68, 1, 82, 34819, 1, 83, 37774, 61, 1, 81, 1595067337, 1, 82, 54857, 1, 83, 35382, 61, 0, 29, 39, 3265120685, 11, 30, 24, 29, 5, 24, 30, 26, 31, 22, 24, 34, 85, 31, 66, 70, 1, 81, 4003251096, 1, 86, 1492533502, 67, 75, 68, 1, 82, 46696, 1, 83, 63727, 61, 1, 81, 1304993952, 1, 82, 25822, 1, 83, 52516, 61, 0, 32, 40, 1624320382, 12, 33, 24, 32, 5, 24, 33, 26, 34, 22, 24, 34, 85, 34, 66, 70, 1, 81, 2385150232, 1, 86, 2516875418, 67, 82, 68, 1, 82, 5122, 1, 83, 41917, 61, 1, 81, 1796045342, 1, 82, 63946, 1, 83, 64135, 61, 0, 35, 41, 982514630, 11, 36, 24, 35, 5, 24, 36, 26, 37, 22, 24, 34, 85, 37, 66, 1, 81, 2339093063, 1, 86, 3660310355, 67, 81, 68, 1, 82, 24064, 1, 83, 6388, 61, 1, 81, 80746225, 1, 82, 25288, 1, 83, 32052, 61, 0, 38, 42, 195052464, 12, 39, 24, 38, 5, 24, 39, 26, 40, 22, 24, 34, 85, 40, 66, 1, 81, 2102027855, 1, 86, 54335912, 67, 81, 68, 1, 82, 26196, 1, 83, 107, 61, 1, 81, 1704550681, 1, 82, 49374, 1, 83, 28676, 61, 0, 41, 43, 2239278522, 12, 42, 24, 41, 5, 24, 42, 26, 43, 22, 24, 34, 85, 43, 66, 1, 81, 4075754391, 1, 86, 2291510320, 67, 77, 68, 1, 82, 29527, 1, 83, 36547, 61, 1, 81, 1238875085, 1, 82, 59344, 1, 83, 48154, 61, 0, 44, 44, 2982365171, 11, 45, 24, 44, 5, 24, 45, 26, 46, 22, 24, 34, 85, 46, 66, 1, 81, 394572473, 1, 86, 989321326, 67, 77, 68, 1, 82, 54986, 1, 83, 62681, 61, 1, 81, 2556366653, 1, 82, 52423, 1, 83, 59965, 61, 0, 47, 45, 1249519284, 12, 48, 24, 47, 5, 24, 48, 26, 49, 22, 24, 34, 85, 49, 66, 1, 81, 776674383, 1, 86, 3750423379, 67, 77, 68, 1, 82, 37567, 1, 83, 3199, 61, 1, 81, 2560613923, 1, 82, 6820, 1, 83, 42937, 61, 0, 50, 46, 611707965, 12, 51, 24, 50, 5, 24, 51, 26, 52, 22, 24, 34, 85, 52, 66, 70, 1, 81, 2144157755, 1, 86, 1876707642, 67, 75, 68, 1, 82, 59703, 1, 83, 53588, 61, 1, 81, 2846328370, 1, 82, 11809, 1, 83, 7731, 61, 0, 53, 47, 2995627984, 11, 54, 24, 53, 5, 24, 54, 26, 55, 22, 24, 34, 85, 55, 66, 1, 81, 3085379198, 1, 86, 1764160402, 67, 77, 68, 1, 82, 43103, 1, 83, 49204, 61, 1, 81, 4241832059, 1, 82, 551, 1, 83, 24735, 61, 0, 56, 48, 589824350, 12, 57, 24, 56, 5, 24, 57, 26, 58, 22, 24, 34, 85, 58, 66, 1, 81, 502748960, 1, 86, 371027870, 67, 77, 68, 1, 82, 50997, 1, 83, 7004, 61, 1, 81, 4142613282, 1, 82, 10368, 1, 83, 59663, 61, 0, 59, 49, 4241374529, 11, 60, 24, 59, 5, 24, 60, 26, 61, 22, 24, 34, 85, 61, 66, 70, 1, 81, 3676555008, 1, 86, 2355337844, 67, 75, 68, 1, 82, 7770, 1, 83, 3509, 61, 1, 81, 2817659480, 1, 82, 43857, 1, 83, 39251, 61, 0, 62, 50, 1110808927, 12, 63, 24, 62, 5, 24, 63, 26, 64, 22, 24, 34, 85, 64, 66, 70, 1, 81, 1445564543, 1, 86, 850816566, 67, 82, 68, 1, 82, 61893, 1, 83, 59866, 61, 1, 81, 1220818844, 1, 82, 64622, 1, 83, 35881, 61, 0, 65, 51, 89624959, 11, 66, 24, 65, 5, 24, 66, 26, 67, 22, 24, 34, 85, 67, 66, 70, 1, 81, 2459714510, 1, 86, 4243967272, 67, 75, 68, 1, 82, 52284, 1, 83, 43374, 61, 1, 81, 2916498653, 1, 82, 14893, 1, 83, 32795, 61, 0, 68, 52, 4076760130, 11, 69, 24, 68, 5, 24, 69, 26, 70, 22, 24, 34, 85, 70, 66, 70, 1, 81, 2744043925, 1, 86, 696351029, 67, 82, 68, 1, 82, 18841, 1, 83, 5592, 61, 1, 81, 3538858232, 1, 82, 9038, 1, 83, 30889, 61, 0, 71, 53, 4061141187, 12, 72, 24, 71, 5, 24, 72, 26, 73, 22, 24, 34, 85, 73, 66, 70, 1, 81, 934622532, 1, 86, 1817131259, 67, 75, 68, 1, 82, 27219, 1, 83, 35682, 61, 1, 81, 2408846276, 1, 82, 65402, 1, 83, 497, 61, 0, 74, 54, 3824370806, 12, 75, 24, 74, 5, 24, 75, 26, 76, 22, 24, 34, 85, 76, 66, 70, 1, 81, 2229081179, 1, 86, 181392570, 67, 75, 68, 1, 82, 47921, 1, 83, 10422, 61, 1, 81, 69319958, 1, 82, 38991, 1, 83, 53018, 61, 0, 77, 55, 2497155311, 11, 78, 24, 77, 5, 24, 78, 26, 79, 22, 24, 34, 85, 79, 66, 70, 1, 81, 972518846, 1, 86, 3682821017, 67, 75, 68, 1, 82, 45673, 1, 83, 15604, 61, 1, 81, 4026247739, 1, 82, 21039, 1, 83, 9019, 61, 1, 81, 2821120979, 1, 82, 27923, 1, 83, 24819, 61, 126, 127, 43, 88, 14, 15, 1, 87, 11, 18, 6, 88, 1, 80, 24792, 76, 1, 81, 1479889443, 1, 82, 29486, 1, 83, 14388, 61, 1, 80, 12016, 128, 34, 85, 80, 66, 70, 1, 81, 2792368653, 1, 86, 1008118699, 67, 75, 68, 1, 82, 19579, 1, 83, 14243, 61, 1, 80, 37928, 71, 1, 81, 623265871, 1, 82, 20904, 1, 83, 43481, 61, 1, 80, 4087, 76, 1, 81, 1468452365, 1, 82, 51862, 1, 83, 3040, 61, 0, 87, 34, 3249455136, 8, 15, 14, 87, 0, 16, 29, 4029277889, 1, 80, 36036, 71, 1, 81, 2451167335, 1, 82, 31542, 1, 83, 52397, 61, 129, 34, 85, 87, 66, 70, 1, 81, 1013158695, 1, 86, 2334376019, 67, 75, 68, 1, 82, 31165, 1, 83, 19664, 61, 1, 80, 38392, 71, 1, 81, 464911378, 1, 82, 22119, 1, 83, 755, 61, 1, 80, 25806, 71, 1, 81, 4119732425, 1, 82, 261, 1, 83, 4343, 61, 130, 131, 0, 13, 56, 2777170973, 1, 80, 10186, 76, 1, 81, 3520361619, 1, 82, 22647, 1, 83, 8884, 61, 5, 87, 7, 0, 87, 30, 1127567711, 11, 19, 7, 87, 1, 80, 40046, 71, 1, 81, 345515061, 1, 82, 54383, 1, 83, 36159, 61, 132, 5, 1, 87, 0, 11, 57, 1274276969, 1, 80, 4600, 76, 1, 81, 3008932197, 1, 82, 39590, 1, 83, 64531, 61, 1, 80, 21443, 71, 1, 81, 1846726385, 1, 82, 14066, 1, 83, 19487, 61, 5, 6, 18, 1, 80, 12046, 71, 1, 81, 1899843561, 1, 82, 41613, 1, 83, 58241, 61, 8, 87, 0, 16, 11, 88, 2, 7, 43, 17, 0, 87, 1, 88, 1, 80, 23575, 76, 1, 81, 2274610014, 1, 82, 42490, 1, 83, 55626, 61, 0, 87, 6, 0, 45, 87, 16, 87, 12, 13, 5, 8, 87, 133, 1, 80, 36473, 76, 1, 81, 2498962184, 1, 82, 18004, 1, 83, 45377, 61, 0, 87, 8, 3930026717, 5, 7, 87, 1, 80, 45251, 76, 1, 81, 1761298278, 1, 82, 53971, 1, 83, 13054, 61, 1, 80, 6145, 71, 1, 81, 1848669688, 1, 82, 64197, 1, 83, 8305, 61, 0, 87, 58, 1263769051, 11, 9, 1, 87, 0, 10, 8, 3930026717, 1, 80, 37140, 71, 1, 81, 912881552, 1, 82, 44390, 1, 83, 20645, 61, 4, 5, 0, 87, 35, 511988258, 5, 6, 87, 1, 80, 27465, 71, 1, 81, 129953103, 1, 82, 39443, 1, 83, 14117, 61, 5, 7, 19, 1, 80, 51448, 76, 1, 81, 1548598989, 1, 82, 22, 1, 83, 35552, 61, 42, 81, 84, 3, 81, 82, 83, 134, 4, 4, 0, 6, 59, 2750526094, 135, 136, 0, 7, 8, 3930026717, 137, 5, 5, 8, 2, 6, 60, 2543448068, 0, 7, 61, 1215546456, 8, 8, 6, 7, 0, 7, 62, 2226522486, 43, 9, 6, 8, 2, 0, 7, 0, 6, 8, 3930026717, 138, 5, 0, 7, 0, 6, 63, 1691330938, 12, 7, 0, 6, 0, 6, 8, 3930026717, 139, 5, 0, 8, 0, 6, 64, 3686677139, 21, 7, 0, 6, 140, 0, 7, 8, 3930026717, 21, 8, 6, 7, 5, 0, 8, 0, 6, 4, 466196368, 141, 0, 6, 65, 3303879103, 21, 8, 0, 6, 142, 0, 7, 8, 3930026717, 21, 8, 6, 7, 5, 0, 8, 11, 6, 0, 5, 0, 7, 8, 3930026717, 21, 8, 6, 7, 5, 0, 8, 0, 6, 66, 3407243665, 11, 7, 0, 6, 0, 6, 8, 3930026717, 21, 8, 7, 6, 5, 0, 8, 45, 0, 0, 6, 6, 0, 45, 6, 4, 4, 0, 6, 59, 2750526094, 143, 17, 6, 1, 7, 0, 7, 8, 3930026717, 21, 8, 6, 7, 5, 5, 8, 0, 6, 67, 2868571576, 21, 7, 0, 6, 144, 0, 7, 8, 3930026717, 21, 8, 6, 7, 5, 0, 8, 2, 6, 60, 2543448068, 0, 7, 61, 1215546456, 8, 8, 6, 7, 0, 7, 68, 155027127, 43, 9, 6, 8, 2, 0, 7, 0, 6, 8, 3930026717, 21, 7, 9, 6, 5, 0, 7, 0, 6, 69, 2858294192, 19, 7, 0, 6, 0, 6, 70, 2189881885, 21, 8, 0, 6, 17, 6, 7, 8, 0, 7, 8, 3930026717, 21, 8, 6, 7, 5, 0, 8, 18, 6, 0, 5, 0, 7, 8, 3930026717, 21, 8, 6, 7, 5, 0, 8, 45, 0, 0, 6, 6, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 14; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // JUMP_2567 (specialized) + regs[fp + SLOTS.PC] = 2567; + break; + } + case 62: + { + // JUMP_1276 (specialized) + regs[fp + SLOTS.PC] = 1276; + break; + } + case 63: + { + // UNARY_POS_77_77 (specialized) + var dst = 77; + regs[base + dst] = +regs[base + 77]; + break; + } + case 64: + { + // SUB_78_78_73 (specialized) + var dst = 78; + var a = regs[base + 78]; + regs[base + dst] = a - regs[base + 73]; + break; + } + case 65: + { + // ADD_73_73_78 (specialized) + var dst = 73; + var a = regs[base + 73]; + regs[base + dst] = a + regs[base + 78]; + break; + } + case 66: + { + // UNARY_POS_85_85 (specialized) + var dst = 85; + regs[base + dst] = +regs[base + 85]; + break; + } + case 67: + { + // SUB_86_86_81 (specialized) + var dst = 86; + var a = regs[base + 86]; + regs[base + dst] = a - regs[base + 81]; + break; + } + case 68: + { + // ADD_81_81_86 (specialized) + var dst = 81; + var a = regs[base + 81]; + regs[base + dst] = a + regs[base + 86]; + break; + } + case 69: + { + // UNARY_NEG_77_77 (specialized) + var dst = 77; + regs[base + dst] = -regs[base + 77]; + break; + } + case 70: + { + // UNARY_NEG_85_85 (specialized) + var dst = 85; + regs[base + dst] = -regs[base + 85]; + break; + } + case 71: + { + // SUB_22_22_80 (specialized) + var dst = 22; + var a = regs[base + 22]; + regs[base + dst] = a - regs[base + 80]; + break; + } + case 72: + { + // BAND_78_78_77 (specialized) + var dst = 78; + var a = regs[base + 78]; + regs[base + dst] = a & regs[base + 77]; + break; + } + case 73: + { + // ADD_17_17_72 (specialized) + var dst = 17; + var a = regs[base + 17]; + regs[base + dst] = a + regs[base + 72]; + break; + } + case 74: + { + // SUB_17_17_72 (specialized) + var dst = 17; + var a = regs[base + 17]; + regs[base + dst] = a - regs[base + 72]; + break; + } + case 75: + { + // BAND_86_85_86 (specialized) + var dst = 86; + var a = regs[base + 85]; + regs[base + dst] = a & regs[base + 86]; + break; + } + case 76: + { + // ADD_22_22_80 (specialized) + var dst = 22; + var a = regs[base + 22]; + regs[base + dst] = a + regs[base + 80]; + break; + } + case 77: + { + // MUL_86_86_85 (specialized) + var dst = 86; + var a = regs[base + 86]; + regs[base + dst] = a * regs[base + 85]; + break; + } + case 78: + { + // MUL_78_78_77 (specialized) + var dst = 78; + var a = regs[base + 78]; + regs[base + dst] = a * regs[base + 77]; + break; + } + case 79: + { + // BAND_78_77_78 (specialized) + var dst = 78; + var a = regs[base + 77]; + regs[base + dst] = a & regs[base + 78]; + break; + } + case 80: + { + // MUL_78_77_78 (specialized) + var dst = 78; + var a = regs[base + 77]; + regs[base + dst] = a * regs[base + 78]; + break; + } + case 81: + { + // MUL_86_85_86 (specialized) + var dst = 86; + var a = regs[base + 85]; + regs[base + dst] = a * regs[base + 86]; + break; + } + case 82: + { + // BAND_86_86_85 (specialized) + var dst = 86; + var a = regs[base + 86]; + regs[base + dst] = a & regs[base + 85]; + break; + } + case 83: + { + // LT_72_17_72 (specialized) + var dst = 72; + var a = regs[base + 17]; + regs[base + dst] = a < regs[base + 72]; + break; + } + case 84: + { + // UNARY_NOT_77_72 (specialized) + var dst = 77; + regs[base + dst] = !regs[base + 72]; + break; + } + case 85: + { + // LOAD_CONST_5_0_3995359512 (specialized) + var dst = 5; + regs[base + dst] = this._constant(0, 3995359512); + break; + } + case 86: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 87: + { + // LOAD_GLOBAL_10_1_2455668248 (specialized) + var dst = 10; + var globalName = this._constant(1, 2455668248); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 88: + { + // LOAD_INT_7_2169651486 (specialized) + var dst = 7; + regs[base + dst] = 2169651486; + break; + } + case 89: + { + // LOAD_INT_8_228 (specialized) + var dst = 8; + regs[base + dst] = 228; + break; + } + case 90: + { + // LOAD_INT_9_11 (specialized) + var dst = 9; + regs[base + dst] = 11; + break; + } + case 91: + { + // LOAD_CONST_11_2_3069285329 (specialized) + var dst = 11; + regs[base + dst] = this._constant(2, 3069285329); + break; + } + case 92: + { + // LOAD_CONST_12_3_3267373394 (specialized) + var dst = 12; + regs[base + dst] = this._constant(3, 3267373394); + break; + } + case 93: + { + // LOAD_CONST_11_4_466196368 (specialized) + var dst = 11; + regs[base + dst] = this._constant(4, 466196368); + break; + } + case 94: + { + // MOD_13_12_11 (specialized) + var dst = 13; + var a = regs[base + 12]; + regs[base + dst] = a % regs[base + 11]; + break; + } + case 95: + { + // SET_PROP_10_2_13 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 10]; + var key = regs[base + 2]; + var val = regs[base + 13]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 96: + { + // RETURN_10 (specialized) + var retVal = regs[base + 10]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 97: + { + // LOAD_THIS_15 (specialized) + var dst = 15; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 98: + { + // MOVE_17_16 (specialized) + var dst = 17; + regs[base + dst] = regs[base + 16]; + break; + } + case 99: + { + // MOVE_19_18 (specialized) + var dst = 19; + regs[base + dst] = regs[base + 18]; + break; + } + case 100: + { + // NEQ_21_17_20 (specialized) + var dst = 21; + var a = regs[base + 17]; + regs[base + dst] = a !== regs[base + 20]; + break; + } + case 101: + { + // UNARY_NOT_77_21 (specialized) + var dst = 77; + regs[base + dst] = !regs[base + 21]; + break; + } + case 102: + { + // LOAD_INT_73_2909999316 (specialized) + var dst = 73; + regs[base + dst] = 2909999316; + break; + } + case 103: + { + // MOVE_19_22 (specialized) + var dst = 19; + regs[base + dst] = regs[base + 22]; + break; + } + case 104: + { + // EQ_23_17_19 (specialized) + var dst = 23; + var a = regs[base + 17]; + regs[base + dst] = a === regs[base + 19]; + break; + } + case 105: + { + // UNARY_NOT_77_23 (specialized) + var dst = 77; + regs[base + dst] = !regs[base + 23]; + break; + } + case 106: + { + // MOVE_19_25 (specialized) + var dst = 19; + regs[base + dst] = regs[base + 25]; + break; + } + case 107: + { + // EQ_26_17_19 (specialized) + var dst = 26; + var a = regs[base + 17]; + regs[base + dst] = a === regs[base + 19]; + break; + } + case 108: + { + // UNARY_NOT_77_26 (specialized) + var dst = 77; + regs[base + dst] = !regs[base + 26]; + break; + } + case 109: + { + // EQ_29_17_19 (specialized) + var dst = 29; + var a = regs[base + 17]; + regs[base + dst] = a === regs[base + 19]; + break; + } + case 110: + { + // EQ_32_17_19 (specialized) + var dst = 32; + var a = regs[base + 17]; + regs[base + dst] = a === regs[base + 19]; + break; + } + case 111: + { + // SHL_79_12_14 (specialized) + var dst = 79; + var a = regs[base + 12]; + regs[base + dst] = a << regs[base + 14]; + break; + } + case 112: + { + // BOR_80_10_79 (specialized) + var dst = 80; + var a = regs[base + 10]; + regs[base + dst] = a | regs[base + 79]; + break; + } + case 113: + { + // GET_PROP_11_3_79 (specialized) + // dst = regs[obj][regs[key]] + var dst = 11; + var obj = regs[base + 3]; + var key = regs[base + 79]; + regs[base + dst] = obj[key]; + break; + } + case 114: + { + // GET_PROP_80_3_79 (specialized) + // dst = regs[obj][regs[key]] + var dst = 80; + var obj = regs[base + 3]; + var key = regs[base + 79]; + regs[base + dst] = obj[key]; + break; + } + case 115: + { + // LT_79_5_80 (specialized) + var dst = 79; + var a = regs[base + 5]; + regs[base + dst] = a < regs[base + 80]; + break; + } + case 116: + { + // RETURN_4 (specialized) + var retVal = regs[base + 4]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 117: + { + // GET_PROP_80_3_79 (specialized) + // dst = regs[obj][regs[key]] + var dst = 80; + var obj = regs[base + 3]; + var key = regs[base + 79]; + regs[base + dst] = obj[key]; + break; + } + case 118: + { + // RETURN_79 (specialized) + var retVal = regs[base + 79]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 119: + { + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 120: + { + // LOAD_GLOBAL_79_32_219639422 (specialized) + var dst = 79; + var globalName = this._constant(32, 219639422); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 121: + { + // LOAD_GLOBAL_7_33_2508669417 (specialized) + var dst = 7; + var globalName = this._constant(33, 2508669417); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 122: + { + // GET_PROP_8_7_79 (specialized) + // dst = regs[obj][regs[key]] + var dst = 8; + var obj = regs[base + 7]; + var key = regs[base + 79]; + regs[base + dst] = obj[key]; + break; + } + case 123: + { + // JUMP_REG_73 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 73]; + break; + } + case 124: + { + // LOAD_THIS_20 (specialized) + var dst = 20; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 125: + { + // NEQ_26_22_25 (specialized) + var dst = 26; + var a = regs[base + 22]; + regs[base + dst] = a !== regs[base + 25]; + break; + } + case 126: + { + // RETURN_6 (specialized) + var retVal = regs[base + 6]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 127: + { + // BXOR_87_17_8 (specialized) + var dst = 87; + var a = regs[base + 17]; + regs[base + dst] = a ^ regs[base + 8]; + break; + } + case 128: + { + // LT_80_22_80 (specialized) + var dst = 80; + var a = regs[base + 22]; + regs[base + dst] = a < regs[base + 80]; + break; + } + case 129: + { + // LT_87_7_3 (specialized) + var dst = 87; + var a = regs[base + 7]; + regs[base + dst] = a < regs[base + 3]; + break; + } + case 130: + { + // USHR_87_1_11 (specialized) + var dst = 87; + var a = regs[base + 1]; + regs[base + dst] = a >>> regs[base + 11]; + break; + } + case 131: + { + // BXOR_12_1_87 (specialized) + var dst = 12; + var a = regs[base + 1]; + regs[base + dst] = a ^ regs[base + 87]; + break; + } + case 132: + { + // BOR_87_9_10 (specialized) + var dst = 87; + var a = regs[base + 9]; + regs[base + dst] = a | regs[base + 10]; + break; + } + case 133: + { + // LOAD_GLOBAL_14_33_2508669417 (specialized) + var dst = 14; + var globalName = this._constant(33, 2508669417); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 134: + { + // JUMP_REG_81 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 81]; + break; + } + case 135: + { + // SHL_7_2_6 (specialized) + var dst = 7; + var a = regs[base + 2]; + regs[base + dst] = a << regs[base + 6]; + break; + } + case 136: + { + // BOR_6_1_7 (specialized) + var dst = 6; + var a = regs[base + 1]; + regs[base + dst] = a | regs[base + 7]; + break; + } + case 137: + { + // USHR_8_6_7 (specialized) + var dst = 8; + var a = regs[base + 6]; + regs[base + dst] = a >>> regs[base + 7]; + break; + } + case 138: + { + // USHR_7_9_6 (specialized) + var dst = 7; + var a = regs[base + 9]; + regs[base + dst] = a >>> regs[base + 6]; + break; + } + case 139: + { + // USHR_8_7_6 (specialized) + var dst = 8; + var a = regs[base + 7]; + regs[base + dst] = a >>> regs[base + 6]; + break; + } + case 140: + { + // BXOR_6_0_7 (specialized) + var dst = 6; + var a = regs[base + 0]; + regs[base + dst] = a ^ regs[base + 7]; + break; + } + case 141: + { + // SHL_7_0_6 (specialized) + var dst = 7; + var a = regs[base + 0]; + regs[base + dst] = a << regs[base + 6]; + break; + } + case 142: + { + // BOR_6_7_8 (specialized) + var dst = 6; + var a = regs[base + 7]; + regs[base + dst] = a | regs[base + 8]; + break; + } + case 143: + { + // SHL_7_2_6 (specialized) + var dst = 7; + var a = regs[base + 2]; + regs[base + dst] = a << regs[base + 6]; + break; + } + case 144: + { + // BXOR_6_0_7 (specialized) + var dst = 6; + var a = regs[base + 0]; + regs[base + dst] = a ^ regs[base + 7]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/qualification.json new file mode 100644 index 00000000..27c8735f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/qualification.json @@ -0,0 +1,64 @@ +{ + "id": "arithmetic--stringConcealingControlFlowFlatteningDispatcherConcealConstantsSpecializedOpcodes--88", + "case": "arithmetic", + "profile": "stringConcealingControlFlowFlatteningDispatcherConcealConstantsSpecializedOpcodes", + "seed": 88, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": true, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": { + "kind": "value", + "value": 0 + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": null, + "components": { + "stringConcealing": null, + "controlFlowFlattening": null, + "dispatcher": null, + "concealConstants": null, + "specializedOpcodes": null + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/source.js b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-cff-dispatcher-concealed-constants-specialized-arithmetic/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/encoded.js new file mode 100644 index 00000000..3c42620a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/encoded.js @@ -0,0 +1,1206 @@ +var CONSTANTS = [/* 0 */"3z26SK", /* 1 */31313, /* 2 */33777, /* 3 */"DVApXyGOuQH6MXH2ptxJPS5BHgv5LJySafBuq6+cNSViSY8Ot09+E/MqtCak3WDQK6whdoYe5Eku6cwU0Zj9UUJONJyxdWOKHpuk11yXSSL1/rqYjckFGp/w7WhvqZ1tinAXx+T11Gp0FE4NO2hQota5OZc0rth7V33QYMtN16uTijTtBesV7CKruKNPICjG3UuC8o9DVCdUrdAlL3K5Fmp/SkSqd5doqgBvWIZJFSAFCnfLVYpHxZVpWJOi6fNUyIDeJGzRRiraL5yBlgSHjdf99R5eqDCajTH6ziJ6C8xODUUOYP/51B0O5PRe4JdO7xMK+1hF4nrd7J0QgXeCCu/ovp/yhv0UIJSrIXpigV9sAcBCLLzoAiCH//PV23W/hYUPdVyODJOAF8gE56yvTlogTm9TQQF8LJx9PZtCZPAi+IS/Z05SUrVVAtG04g5v1BTrEKYRlFelv05aQ4xgH5H9frMkNvkV7mXpXQC9OZtJiUX0vkx0jBq1LlFYxwjERbvaLlhF7mXPNZGSitkCYKJlnW+kNAHL4hWUde2NySrQwdNiJU75/iHLnmiHtTZDDqSBho6kgVdh/snL8/4DqqXU3/NQy52j1i7KotcZLZHW+tOHwgvmvu0q/OH+eZmdN6mtJhgD50r6VHaYwTPp4dxYwe2zqtPfikUIzP95KQpjm1rTcNgb/lWpzOlkRZa9FWPy4Yqj09mWA1yAa6dPY7X5V8UMTZZaYkAkU+YvyDj+Sw1Kx7HEJ8BlHhhkd2uLDB12nJVO1ChhnhdFTbD89qstI3Jjl7Utm8PIx2lZjmcdsV1OfwZTXzDlEj7nxiYe/vb2MWXEwYDDJdnPDAykry4/ETwgw9rQBcuV1zCIEoGyODOSSBKzr2N3m0LxNYHCfKE5t1Hr7sadMeO4sAg5Cypkkybbuu3gKetFsdg1tJRLGN+gE8/qcWihN/TK8g5WbiqTm4AXWW91G7SycF1q2o+PtT3cE4dj7O5pbGNzIOhs8dSJDz2RFrdw7nneuNZT9eq/0vR4GMO6zcwkXW9VDGhiSL8oKsghHk2gV4FKfOgLDoEG+etQLQ+aOcqn+2/M7o+tTiHPGe1G2ek0dq0MaAMY3afjXGttDkBzZBsfT8jwU8okrOx3l4P0JqK4cQV7/FdsHKa5", /* 4 */"window", /* 5 */7, /* 6 */undefined, /* 7 */59462, /* 8 */12231, /* 9 */"q4T1VxR", /* 10 */"6sM1", /* 11 */"pQBYNjM", /* 12 */"03I", /* 13 */"qThCLig", /* 14 */"atob", /* 15 */"", /* 16 */0, /* 17 */"length", /* 18 */"Function", /* 19 */"String", /* 20 */"fromCharCode", /* 21 */"charCodeAt", /* 22 */1, /* 23 */"XY7KhZT5Um", /* 24 */8, /* 25 */"5PAzwaN", /* 26 */"eXq3OfK", /* 27 */2, /* 28 */"iudeFm", /* 29 */36769, /* 30 */11492, /* 31 */"PFF", /* 32 */"neI73q0g", /* 33 */"Number", /* 34 */2654435769, /* 35 */"3zKscnnD", /* 36 */"sJ5bdzhwa0", /* 37 */13, /* 38 */65535, /* 39 */"Tp7NkXPU9j", /* 40 */"9CGdWyx", /* 41 */"bgAW2eHA", /* 42 */"Fjs81G93"]; +var BYTECODE = [0, 12, 0, 0, 0, 13, 1, 0, 0, 14, 2, 0, 47, 6, 236, 1, 15, 0, 0, 0, 7, 3, 0, 42, 5, 6, 1, 7, 47, 8, 464, 4, 19, 0, 0, 4, 1, 47, 15, 121, 1, 4, 1, 0, 1, 5, 61, 2, 15, 12, 0, 12, 14, 13, 14, 2, 15, 4, 0, 1, 9, 421665429, 1, 10, 228, 1, 11, 11, 42, 3, 8, 4, 5, 9, 10, 11, 1, 9, 3171613076, 1, 10, 240, 1, 11, 2, 42, 4, 8, 4, 5, 9, 10, 11, 42, 16, 2, 1, 4, 0, 17, 5, 0, 42, 18, 16, 1, 17, 42, 16, 18, 0, 9, 15, 3, 16, 0, 15, 6, 0, 45, 15, 4, 2, 47, 3, 142, 1, 4, 2, 0, 1, 0, 0, 0, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 47, 3, 165, 0, 13, 3, 0, 0, 0, 1, 0, 0, 1, 45, 3, 0, 3, 6, 0, 45, 3, 0, 8, 7, 0, 0, 9, 8, 0, 3, 3, 2, 47, 4, 464, 4, 19, 0, 0, 4, 1, 3, 10, 0, 1, 5, 3332831354, 1, 6, 244, 1, 7, 1, 42, 2, 4, 4, 3, 5, 6, 7, 62, 8, 2, 8, 8, 0, 11, 10, 9, 9, 3, 10, 1, 62, 8, 10, 9, 8, 0, 12, 11, 10, 8, 45, 12, 0, 10, 6, 0, 45, 10, 0, 6, 11, 0, 0, 8, 12, 0, 0, 7, 13, 0, 4, 2, 2, 9, 14, 0, 42, 10, 9, 1, 0, 61, 3, 10, 7, 1, 6, 8, 6, 8, 0, 9, 15, 0, 61, 4, 9, 6, 1, 6, 8, 6, 6, 0, 9, 16, 0, 61, 5, 9, 8, 1, 8, 6, 6, 6, 0, 9, 17, 0, 8, 10, 3, 9, 63, 5, 18, 10, 6, 6, 0, 6, 7, 9, 6, 8, 6, 40, 9, 456, 2, 9, 19, 0, 0, 10, 20, 0, 8, 11, 9, 10, 0, 10, 21, 0, 8, 12, 3, 10, 43, 10, 3, 12, 1, 5, 0, 12, 21, 0, 8, 13, 3, 12, 0, 12, 22, 0, 62, 7, 12, 6, 7, 0, 14, 5, 23, 8, 43, 12, 3, 13, 1, 14, 0, 13, 24, 0, 64, 8, 7, 14, 6, 6, 6, 1, 7, 6, 13, 6, 6, 6, 7, 8, 12, 65, 12, 7, 14, 8, 7, 0, 10, 25, 8, 7, 7, 8, 43, 10, 9, 11, 1, 12, 62, 8, 10, 7, 7, 0, 9, 4, 26, 8, 61, 4, 9, 7, 1, 6, 7, 6, 7, 0, 9, 27, 0, 62, 6, 9, 6, 6, 0, 10, 5, 28, 6, 61, 5, 10, 6, 0, 7, 6, 6, 6, 39, 294, 45, 4, 0, 9, 6, 0, 45, 9, 0, 12, 29, 0, 0, 10, 30, 0, 0, 11, 31, 0, 0, 9, 32, 0, 0, 13, 16, 0, 4, 5, 0, 14, 15, 0, 61, 6, 14, 10, 0, 10, 11, 10, 10, 0, 14, 16, 0, 61, 7, 14, 10, 2, 10, 9, 10, 9, 63, 7, 33, 3, 10, 9, 0, 10, 11, 14, 9, 11, 10, 40, 14, 715, 0, 14, 34, 0, 62, 10, 14, 11, 9, 0, 15, 1, 35, 9, 0, 14, 16, 0, 65, 16, 11, 14, 9, 11, 0, 15, 36, 9, 10, 10, 10, 61, 1, 16, 10, 0, 10, 9, 9, 9, 0, 14, 37, 0, 66, 12, 580, 10, 14, 10, 15, 1, 67, 14, 10, 15, 9, 10, 10, 1, 10, 0, 15, 38, 0, 68, 14, 39, 11, 10, 0, 16, 15, 10, 61, 8, 16, 10, 0, 9, 11, 10, 9, 2, 14, 19, 0, 0, 15, 20, 0, 8, 16, 14, 15, 0, 15, 21, 0, 8, 17, 0, 15, 62, 10, 7, 11, 9, 0, 15, 2, 40, 9, 43, 18, 0, 17, 1, 15, 67, 15, 9, 8, 9, 11, 10, 18, 10, 43, 17, 14, 16, 1, 15, 62, 9, 17, 9, 9, 0, 14, 6, 41, 10, 61, 6, 14, 9, 0, 11, 10, 11, 10, 61, 14, 7, 10, 0, 10, 10, 10, 10, 0, 14, 22, 0, 62, 9, 14, 9, 10, 0, 15, 7, 42, 10, 61, 7, 15, 11, 2, 10, 9, 9, 10, 39, 512, 45, 6, 0, 14, 6, 0, 45, 14]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 19; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 61: + { + // ANTI_MOVE_3_4_2_6_0_5_1_7 (order: [3,4,2,6,0,5,1,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[7]]; + // BOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a | regs[base + _operands[2]]; + // MOVE + var dst = _operands[3]; + regs[base + dst] = regs[base + _operands[4]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[5]; + var count = _operands[6]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[7]]; + regs[base + dst] = elems; + break; + } + case 62: + { + // ANTI_ADD_8_5_6_0_2_3_4_1_7 (order: [8,5,6,0,2,3,4,1,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[0]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[0]; + var name = this._constant(_operands[1], _operands[2]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // ADD + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a + regs[base + _operands[5]]; + // BAND + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a & regs[base + _operands[8]]; + break; + } + case 63: + { + // ANTI_LT_10_7_11_2_4_8_1_5_9_3_6_0 (order: [10,7,11,2,4,8,1,5,9,3,6,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[11], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[9], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[2]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[5]]); + // LOAD_GLOBAL + var dst = _operands[6]; + var globalName = this._constant(_operands[7], _operands[8]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LT + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a < regs[base + _operands[11]]; + break; + } + case 64: + { + // ANTI_SHL_3_14_0_5_9_12_4_13_10_2_6_8_11_7_15_1 (order: [3,14,0,5,9,12,4,13,10,2,6,8,11,7,15,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[15], _unsortedOperands[9], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[10], _unsortedOperands[13], _unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[12], _unsortedOperands[5], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[14]]; + // SHL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a << regs[base + _operands[2]]; + // BUILD_OBJECT + // dst, pairCount, [keyReg, valReg, ...] + var dst = _operands[3]; + var pairCount = _operands[4]; + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + _operands[5]]; + var val = regs[base + _operands[6]]; + o[key] = val; + } + regs[base + dst] = o; + // ADD + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a + regs[base + _operands[9]]; + // DIV + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a / regs[base + _operands[12]]; + // MUL + var dst = _operands[13]; + var a = regs[base + _operands[14]]; + regs[base + dst] = a * regs[base + _operands[15]]; + break; + } + case 65: + { + // ANTI_BOR_0_5_2_4_11_8_1_7_3_9_10_6 (order: [0,5,2,4,11,8,1,7,3,9,10,6]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[8], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[11], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[9], _unsortedOperands[10], _unsortedOperands[4]]; + // BOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a | regs[base + _operands[2]]; + // DIV + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a / regs[base + _operands[5]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[6]; + var name = this._constant(_operands[7], _operands[8]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + // BAND + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a & regs[base + _operands[11]]; + break; + } + case 66: + { + // ANTI_USHR_2_3_1_6_0_4_5 (order: [2,3,1,6,0,4,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[3]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // JUMP_IF_FALSE + var src = _operands[2]; + var target = _operands[3]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // USHR + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a >>> regs[base + _operands[6]]; + break; + } + case 67: + { + // ANTI_BXOR_3_1_5_2_7_6_4_0 (order: [3,1,5,2,7,6,4,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[4]]; + // BAND + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a & regs[base + _operands[2]]; + // BXOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a ^ regs[base + _operands[5]]; + // MOVE + var dst = _operands[6]; + regs[base + dst] = regs[base + _operands[7]]; + break; + } + case 68: + { + // ANTI_BAND_3_6_5_0_7_2_4_1 (order: [3,6,5,0,7,2,4,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[4]]; + // UNARY_BITNOT + var dst = _operands[0]; + regs[base + dst] = ~regs[base + _operands[1]]; + // BAND + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a & regs[base + _operands[4]]; + // TYPEOF_SAFE + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = _operands[5]; + var name = this._constant(_operands[6], _operands[7]); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/qualification.json new file mode 100644 index 00000000..ca5a2808 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/qualification.json @@ -0,0 +1,61 @@ +{ + "id": "nestedStrings--stringConcealingAntiInstrumentation--81", + "case": "nestedStrings", + "profile": "stringConcealingAntiInstrumentation", + "seed": 81, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": "id:7" + }, + "encodedObservation": { + "kind": "value", + "value": "id:7" + }, + "decodedObservation": { + "kind": "value", + "value": "id:7" + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "antiInstrumentation": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/source.js new file mode 100644 index 00000000..fbfe36eb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-anti-instrumentation-nested/source.js @@ -0,0 +1 @@ +function outer(prefix) { return function middle(n) { return function inner() { return prefix + ":" + n; }; }; } window.TEST_OUTPUT = outer("id")(7)(); diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/encoded.js new file mode 100644 index 00000000..f3d9ef4f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"R58TC0aBexkIsbA4A6PHb0nz+lgqwaq5dVEx67Z/CeuxED25pAEWmYDwPW/kI5+46jCL2ZFB9MheX8crLKe7GN3x/XnagOAI1rPlJ/Jg0vjbUSHIqEDrS/bXkHjl4PyZ+THtiGsAu/cHcJiYQSC5qSPQqkgVwA9YgLAxOLqhKejKcAcI+INoOHiQF2gI4EVoENA1OAtAo9g3cHfoIyBuqEYwW5hUAFSIKxA7qEYgT9ibb2H4UEB9yHywdShn4E54aJCPCCaAsAkJkJz4FWCZ20qwa0ihwR9IqvAbWJAjMaiyEHKI4wHsmPsQ/LvUJ9VoBPC5SQDA7DjyU+XX52DV6PeR5znogMAYxfdaa6mg0CnfsdjY50Djx/1TjahgIdiZBRG9eHgHlIsMP++o8WE5+Y5QYNe5QyNPn9A9eY/hAhl0t2ALbp9IC3xxrhkdoac4M9NjT0jDIFlu0Uy5NSEz60h/P4uFBge5oxF8mVXjaG8X00q3S0GH2Q5xzeo9X4QrBLfrGgGBjXnyknkvp6MNJ45SCvmFQQ7JqT7tq57n23rW7v2ZxwGmjkITqPcnY//eqTHnSfLe1krs9/lb366mOsOREQ7mgscHxZPyPtKi6Wne8a5KRcatuwdOgNoEZqHpAxK5tooDA56p8jF2jeErqpI2PNq+T1r6TlZHKkGidSZTkoB/GIKgBnqyfCp2ZnX6aG512mNGS0pd3gZ2zuIEnkwyQbZoAhOOUnZEmlEus7s51opKQf6fRRzSqj5/om3mqHJwDo2FEjqEjkAaoecAauzezTXIRZTe/3K1995jiK4wMsWV1A62eu8XwKuHPtKq81XX/b5CG8ecsxUv0OLxZfzt+hrPhpsLNJ+66gVlut0creM2aMPlTyTzFFX2LR698xYVi9p/KYvdFgy9li1VZWTzEk8aw2pHH02G3Qtls+pdn1oLSIdeHUiNb3Vu5Wgvf7M118FMWv1NVVLVdD9Rq1EXdHsTDduFEDWJrCJj4ec7c4HcfkU1RazcH2uP52cbl6wQNZqFLg3/fOMX1qPYPPPU6VUs/ZhMANfeu9os0+TBdeWN0xzFhP4zvJ/U5Bpku90MTM1EwNvST9j0kGTLLTu83hQ0hLt/PpTxFG+8qS0NVAL0408XxK80Oayo3TBkpPQqnGIEaoRBHFzsX2R/5FgsursWxEBMVPxLVHykVDxwrHoUT3xNDIGVKTSKrGZcaPRJTFLcFkRuNL7cPWyi5wIcwoxjBKmFFgy7fCbkEqyxPCDUjUVC/IZMOdScuw0s7pTRdfuN5BzvtKQzFZyD5BB03N2sTMJEzNvBb/Pk0mTKLbu9uQQFg95/35TcJN697S3PVODEmE882580Fa2lHTR1sfQKn/AD8rQqHbDtKWWb1BpPl7tUx6pMfP1pZRWlVD9fk1YHU4MbDduVRiUlryhbWfcwSzvdabVpNVHdX2PE9x4Tp45dBUCVau1he2nnHKuWPjjFxEUd/bVSHie9ozsvtpKNBTuNmRo9t6wrQF/O6jN1k93hQve27dPRb5bi05aYDR69wQbpi+p/4urqJvm99c3cRvTC8k8S0qQmEaKnHel2mAr8n+sL4baQGrntR3aw0l1OtUMz1pZSHP6FZe2qAj7oi2AGvXoz7oKlByLrrkFSScZyQhjefbJ2KoCeCWrP904aX45XOnSScO5EYjjWMFJHPgTCs0kj/kBqTiYZumUOEZpBBimOhhIVppsjTl6b8i5GosEWSoa2odsDbpb6LpaHCRKCpxYVkpR/7uLkVuixxMnARvT6hW8K2pom7amtEcWGwgLKn9wz/qbiIt3pE3Gx2jhP4Dv21u9q6eH1aeySaT+AgzcXlHIg6byhPjmLziRbFselSjDRsKkdId+eIHOAhyIDCIlRMXeZbuEBe3PXLUuFvhzJdFFC+V1jZxd5s1IPd4FJeSzxbBkcp54rHl9OyWpBd9lNMXSnQyMJb4fwH5mqASq5nRAM67p/N+tuUbi5N8G7WDbzq28No6k4DrHQGeWA8wjkn9YrtaBdWaTxJXmkwNtP4DO62PlgtvnLEeCI5wCViy0QpXgP4NVZgzDuyNBDmve3cDvYl6EmKKGQFAjvhyIYjrAjOHKgoug6cLHHK0CauC/Q72kT4EdoUBB2uBkAKJh+cCfohSAwqDkwVVhywDJIetBSKCagLBg5cK/4NYDVCVCwTChI4GF4dtBXSLzBPMiEUBr4vm8nmJIwC4iHAZGIHPCOnwsgpqhfUJDJAUCO2MFPnDuToPBo7PH6CayAXDiwkxvoy2D16e9R9zi0wONb8PfWaO0gmKkyMYmY9APeh8QQ86jXIYBZu9AVe8THCkv8sAkpnWEh+aiQhhcvg5UIEJH+eTrhpFgat5TLYV8XyWZxVdlzIXynQRMRj7MARJlLEXq5ZidY6zx/QstrQWY5INHaVhPju29Lkx05dIF9GVlxeevpnykrvbYhWeXBCXmz0h2vrSOMpmYRkvkbAYqKLT+WqwHnlnrU0c1Z78L9zsBT3PeWYh0ZnLHGCuGexQvK98IawK7eOa1R2M4mQp3HWlKKvuwh3mnP/peKmQc1up0OOJqgYSTu8VIptpzDLV5q8qlpYt6MKni2lhsQjqP6DJbpKRuiaCZc0kV+csI7RqcxH6rAZmP6em5jmnkGcIokEn12d+J9Xioyy7YeQmDKEnZF2heuVDpoliwKmP1LBraSDz6aoyfmknJ0ztNF/LpszoJbK+aoagUeirVpAsGe7HP799Bi+66sNeRVps44etvXpyqvvsglxpXN/r6C6gfns8YuRuWkZRDt/lr+x+jLxF736u9lBZ2gMhS3jAN6j5DyYpX9LTe9CVe4yxx/ksoJVco9JK0=", /* 1 */"ttElaZXgEhuijyY7", /* 2 */-340824449, /* 3 */886659389, /* 4 */undefined, /* 5 */1991421705, /* 6 */464038654, /* 7 */2086762865, /* 8 */-1639746937, /* 9 */1395950710, /* 10 */1277860939, /* 11 */437726480, /* 12 */-877099505, /* 13 */-1806874616, /* 14 */1650093267, /* 15 */-1769861712, /* 16 */-552989736, /* 17 */-2055791638, /* 18 */266045370, /* 19 */-799495676, /* 20 */-1919206894, /* 21 */888320832, /* 22 */1676972761, /* 23 */1808431502, /* 24 */908146366, /* 25 */-1477186685, /* 26 */-23573207, /* 27 */-235403459, /* 28 */"x5X1BAmM4RPntA==", /* 29 */true, /* 30 */571531042, /* 31 */1691467921, /* 32 */-1352250015, /* 33 */-842151599, /* 34 */1687132203, /* 35 */-452803850, /* 36 */477700901, /* 37 */1914506522, /* 38 */-1123341527, /* 39 */1086893758, /* 40 */556800124, /* 41 */-637235289, /* 42 */-895272621, /* 43 */1211421957, /* 44 */1982939959, /* 45 */654703911, /* 46 */-1707305755, /* 47 */-144324034, /* 48 */-1542984071, /* 49 */"yazJI8mnwmw=", /* 50 */"", /* 51 */-1888900094, /* 52 */-999721272, /* 53 */"MFNGz1VDS8VtTUy9hCYHa7z/HIs=", /* 54 */"j9AlWIShEiimkCII", /* 55 */"S3ZS6kkiRrYbMr2fEwKNdjASuak9Jqna", /* 56 */"satPMFmIWwBFeLkQ", /* 57 */"uO93eyKVpg00vQ==", /* 58 */-1218129580, /* 59 */-527381084, /* 60 */-656710748, /* 61 */1413310073, /* 62 */-1704149001, /* 63 */806903901, /* 64 */540983996, /* 65 */-1307933684, /* 66 */1782645934, /* 67 */357211202, /* 68 */1397078762, /* 69 */-313486846, /* 70 */-1500179903, /* 71 */606072185, /* 72 */-343684379, /* 73 */-1418309237, /* 74 */830007689, /* 75 */2100053308, /* 76 */264134628, /* 77 */212164985, /* 78 */59459867, /* 79 */-114965933, /* 80 */1118255337, /* 81 */1177683660, /* 82 */-851041266, /* 83 */"4yD6uJARCYk=", /* 84 */"1wOumDXwsWk=", /* 85 */1251529639, /* 86 */368000552, /* 87 */1665196772, /* 88 */-2057134230, /* 89 */-1457456163, /* 90 */550607659, /* 91 */-1584135157, /* 92 */1050398747, /* 93 */-1756541744, /* 94 */-380023687, /* 95 */-1717944904, /* 96 */-1662454330, /* 97 */254221472]; +var BYTECODE = [47, 6, 1881, 1, 82, 0, 0, 0, 7, 0, 2763460257, 42, 5, 6, 1, 7, 47, 8, 3604, 4, 91, 0, 0, 4, 1, 47, 12, 120, 1, 93, 1, 0, 1, 5, 5, 2, 12, 2, 12, 1, 2933800883, 1, 9, 2819290066, 1, 10, 218, 1, 11, 11, 42, 3, 8, 4, 5, 9, 10, 11, 0, 13, 2, 3954142846, 32, 14, 13, 42, 13, 2, 1, 14, 0, 14, 3, 886659385, 42, 15, 2, 1, 14, 2, 14, 1, 2933800883, 1, 9, 3171117007, 1, 10, 229, 1, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 8, 16, 14, 4, 48, 14, 3, 13, 15, 16, 9, 12, 3, 14, 0, 12, 4, 0, 45, 12, 47, 89, 5340, 3, 10, 0, 0, 4, 19, 0, 20, 5, 1991385880, 5, 21, 20, 0, 22, 6, 464038654, 5, 23, 22, 0, 24, 7, 2086788211, 27, 25, 21, 24, 34, 90, 25, 33, 90, 90, 32, 90, 90, 1, 86, 4202594020, 1, 91, 2509183204, 12, 91, 91, 86, 16, 91, 90, 91, 11, 86, 86, 91, 1, 87, 41830, 1, 88, 44145, 39, 1872, 0, 26, 8, 2655252120, 5, 23, 26, 26, 27, 21, 23, 34, 90, 27, 33, 90, 90, 32, 90, 90, 1, 86, 1004654823, 1, 91, 1828836071, 12, 91, 91, 86, 16, 91, 91, 90, 11, 86, 86, 91, 1, 87, 8549, 1, 88, 64995, 39, 1872, 1, 86, 3157469749, 1, 87, 60855, 1, 88, 50232, 39, 1872, 0, 28, 9, 1395921613, 12, 29, 23, 28, 5, 23, 29, 26, 30, 21, 23, 34, 90, 30, 33, 90, 90, 1, 86, 2431375542, 1, 91, 1380354742, 12, 91, 91, 86, 13, 91, 90, 91, 11, 86, 86, 91, 1, 87, 46388, 1, 88, 62971, 39, 1872, 1, 86, 2120512796, 1, 87, 61086, 1, 88, 64508, 39, 1872, 0, 31, 10, 1277868252, 11, 32, 23, 31, 5, 23, 32, 26, 33, 21, 23, 34, 90, 33, 33, 90, 90, 1, 86, 238576569, 1, 91, 3575144889, 12, 91, 91, 86, 13, 91, 90, 91, 11, 86, 86, 91, 1, 87, 55867, 1, 88, 34446, 39, 1872, 1, 86, 2112406702, 1, 87, 38188, 1, 88, 52717, 39, 1872, 0, 34, 11, 437735174, 11, 35, 23, 34, 5, 23, 35, 26, 36, 21, 23, 34, 90, 36, 33, 90, 90, 32, 90, 90, 1, 86, 2043058627, 1, 91, 3299583939, 12, 91, 91, 86, 16, 91, 91, 90, 11, 86, 86, 91, 1, 87, 44609, 1, 88, 34486, 39, 1872, 1, 86, 3437626277, 1, 87, 2087, 1, 88, 42282, 39, 1872, 0, 37, 12, 3417890068, 12, 38, 23, 37, 5, 23, 38, 26, 39, 21, 23, 34, 90, 39, 33, 90, 90, 32, 90, 90, 1, 86, 3284198216, 1, 91, 222286152, 12, 91, 91, 86, 16, 91, 91, 90, 11, 86, 86, 91, 1, 87, 22218, 1, 88, 46569, 39, 1872, 1, 86, 2327606995, 1, 87, 38225, 1, 88, 857, 39, 1872, 0, 40, 13, 2488098119, 12, 41, 23, 40, 5, 23, 41, 26, 42, 21, 23, 34, 90, 42, 33, 90, 90, 1, 86, 2242930451, 1, 91, 1599104275, 12, 91, 91, 86, 13, 91, 91, 90, 11, 86, 86, 91, 1, 87, 26257, 1, 88, 2228, 39, 1872, 1, 86, 3634817920, 1, 87, 514, 1, 88, 41506, 39, 1872, 0, 43, 14, 1650091691, 11, 44, 23, 43, 5, 23, 44, 26, 45, 21, 23, 34, 90, 45, 33, 90, 90, 32, 90, 90, 1, 86, 2197972571, 1, 91, 3335866459, 12, 91, 91, 86, 16, 91, 91, 90, 11, 86, 86, 91, 1, 87, 6617, 1, 88, 32959, 39, 1872, 1, 86, 523694188, 1, 87, 34798, 1, 88, 10810, 39, 1872, 0, 46, 15, 2525137423, 11, 47, 23, 46, 5, 23, 47, 26, 48, 21, 23, 34, 90, 48, 33, 90, 90, 1, 86, 3302233457, 1, 91, 2566988657, 12, 91, 91, 86, 13, 91, 90, 91, 11, 86, 86, 91, 1, 87, 61171, 1, 88, 27458, 39, 1872, 1, 86, 1338977118, 1, 87, 30428, 1, 88, 14436, 39, 1872, 0, 49, 16, 3741996404, 12, 50, 23, 49, 5, 23, 50, 26, 51, 21, 23, 34, 90, 51, 33, 90, 90, 1, 86, 1212545032, 1, 91, 2391975432, 12, 91, 91, 86, 13, 91, 90, 91, 11, 86, 86, 91, 1, 87, 49034, 1, 88, 39800, 39, 1872, 1, 86, 3760396596, 1, 87, 41654, 1, 88, 55773, 39, 1872, 0, 52, 17, 2239178013, 12, 53, 23, 52, 5, 23, 53, 26, 54, 21, 23, 34, 90, 54, 33, 90, 90, 32, 90, 90, 1, 86, 263968590, 1, 91, 3064956238, 12, 91, 91, 86, 16, 91, 90, 91, 11, 86, 86, 91, 1, 87, 4812, 1, 88, 51576, 39, 1872, 1, 86, 907559064, 1, 87, 24858, 1, 88, 37511, 39, 1872, 0, 55, 18, 266026642, 11, 56, 23, 55, 5, 23, 56, 26, 57, 21, 23, 34, 90, 57, 33, 90, 90, 32, 90, 90, 1, 86, 1499890742, 1, 91, 2672218678, 12, 91, 91, 86, 16, 91, 91, 90, 11, 86, 86, 91, 1, 87, 51124, 1, 88, 27095, 39, 1872, 1, 86, 2211821194, 1, 87, 51464, 1, 88, 27120, 39, 1872, 0, 58, 19, 3495481611, 12, 59, 23, 58, 5, 23, 59, 26, 60, 21, 23, 34, 90, 60, 33, 90, 90, 32, 90, 90, 1, 86, 544018869, 1, 91, 1376785333, 12, 91, 91, 86, 16, 91, 90, 91, 11, 86, 86, 91, 1, 87, 32823, 1, 88, 16218, 39, 1872, 1, 86, 3009741046, 1, 87, 19316, 1, 88, 64512, 39, 1872, 0, 61, 20, 2375772467, 11, 62, 23, 61, 5, 23, 62, 26, 63, 21, 23, 34, 90, 63, 33, 90, 90, 32, 90, 90, 1, 86, 1411609820, 1, 91, 2583861980, 12, 91, 91, 86, 16, 91, 90, 91, 11, 86, 86, 91, 1, 87, 31582, 1, 88, 49618, 39, 1872, 1, 86, 1213063415, 1, 87, 53109, 1, 88, 61973, 39, 1872, 0, 64, 21, 888311553, 12, 65, 23, 64, 5, 23, 65, 26, 66, 21, 23, 34, 90, 66, 33, 90, 90, 1, 86, 3562611159, 1, 91, 300218327, 12, 91, 91, 86, 13, 91, 90, 91, 11, 86, 86, 91, 1, 87, 43605, 1, 88, 38291, 39, 1872, 1, 86, 3890423431, 1, 87, 14085, 1, 88, 46054, 39, 1872, 0, 67, 22, 1676995464, 11, 68, 23, 67, 5, 23, 68, 26, 69, 21, 23, 34, 90, 69, 33, 90, 90, 1, 86, 2435709849, 1, 91, 1389921689, 12, 91, 91, 86, 13, 91, 90, 91, 11, 86, 86, 91, 1, 87, 11291, 1, 88, 51593, 39, 1872, 1, 86, 1578647664, 1, 87, 57330, 1, 88, 32678, 39, 1872, 0, 70, 23, 1808405607, 12, 71, 23, 70, 5, 23, 71, 26, 72, 21, 23, 34, 90, 72, 33, 90, 90, 32, 90, 90, 1, 86, 3012909353, 1, 91, 1858087721, 12, 91, 91, 86, 16, 91, 90, 91, 11, 86, 86, 91, 1, 87, 3243, 1, 88, 31947, 39, 1872, 1, 86, 718985107, 1, 87, 22545, 1, 88, 57444, 39, 1872, 0, 73, 24, 908139037, 12, 74, 23, 73, 5, 23, 74, 26, 75, 21, 23, 34, 90, 75, 33, 90, 90, 1, 86, 1580363135, 1, 91, 282226559, 12, 91, 91, 86, 13, 91, 90, 91, 11, 86, 86, 91, 1, 87, 52477, 1, 88, 44342, 39, 1872, 1, 86, 871134807, 1, 87, 42965, 1, 88, 55508, 39, 1872, 0, 76, 25, 2817748263, 11, 77, 23, 76, 5, 23, 77, 26, 78, 21, 23, 34, 90, 78, 33, 90, 90, 32, 90, 90, 1, 86, 1497983282, 1, 91, 2627569458, 12, 91, 91, 86, 16, 91, 91, 90, 11, 86, 86, 91, 1, 87, 30384, 1, 88, 12535, 39, 1872, 1, 86, 722480705, 1, 87, 45507, 1, 88, 45877, 39, 1872, 0, 79, 26, 4271405317, 11, 80, 23, 79, 5, 23, 80, 26, 81, 21, 23, 34, 90, 81, 33, 90, 90, 1, 86, 625376567, 1, 91, 1727367991, 12, 91, 91, 86, 13, 91, 90, 91, 11, 86, 86, 91, 1, 87, 59061, 1, 88, 11661, 39, 1872, 1, 86, 2777516106, 1, 87, 23496, 1, 88, 2623, 39, 1872, 0, 82, 27, 4059590274, 12, 83, 23, 82, 5, 23, 83, 26, 84, 21, 23, 34, 90, 84, 33, 90, 90, 1, 86, 275435996, 1, 91, 1392873436, 12, 91, 91, 86, 13, 91, 90, 91, 11, 86, 86, 91, 1, 87, 13918, 1, 88, 15387, 39, 1872, 1, 86, 1716968528, 1, 87, 43474, 1, 88, 38291, 39, 1872, 1, 86, 4242692082, 1, 87, 40048, 1, 88, 51405, 39, 1872, 1, 21, 20274, 1, 16, 1763705004, 1, 17, 248, 1, 18, 7, 1, 85, 13195, 12, 21, 21, 85, 1, 86, 2761083645, 1, 87, 383, 1, 88, 37053, 39, 1872, 9, 11, 12, 13, 58, 4, 1, 85, 28799, 12, 21, 21, 85, 1, 86, 2355111145, 1, 87, 61291, 1, 88, 47183, 39, 1872, 0, 92, 2, 3954142846, 11, 9, 0, 92, 55, 1, 85, 18811, 12, 21, 21, 85, 1, 86, 1051631298, 1, 87, 21824, 1, 88, 2689, 39, 1872, 2, 7, 28, 3692678479, 1, 16, 1883372949, 1, 17, 238, 1, 85, 25961, 11, 21, 21, 85, 1, 86, 109008379, 1, 87, 40569, 1, 88, 12880, 39, 1872, 42, 10, 15, 4, 14, 16, 17, 18, 8, 92, 3, 10, 5, 5, 92, 1, 85, 32888, 11, 21, 21, 85, 1, 86, 105625332, 1, 87, 32118, 1, 88, 12900, 39, 1872, 1, 21, 28828, 45, 5, 59, 1687, 4, 5, 1867, 54, 1383, 3, 0, 6, 6, 464038654, 1, 85, 33763, 12, 21, 21, 85, 1, 86, 4171570151, 1, 87, 53349, 1, 88, 52362, 39, 1872, 1, 21, 17401, 45, 5, 1, 18, 8, 42, 8, 15, 4, 14, 16, 17, 18, 44, 92, 7, 1, 8, 46, 92, 3, 14, 0, 47, 15, 3604, 4, 91, 0, 0, 4, 2, 1, 85, 8653, 11, 21, 21, 85, 1, 86, 1526799164, 1, 87, 53438, 1, 88, 28462, 39, 1872, 55, 1, 85, 13670, 11, 21, 21, 85, 1, 86, 375525187, 1, 87, 50369, 1, 88, 8781, 39, 1872, 1, 18, 8, 42, 12, 15, 4, 14, 16, 17, 18, 0, 13, 29, 0, 1, 85, 37178, 11, 21, 21, 85, 1, 86, 3151416438, 1, 87, 2036, 1, 88, 36857, 39, 1872, 1, 4, 1560, 55, 1, 85, 38587, 12, 21, 21, 85, 1, 86, 3552673384, 1, 87, 46570, 1, 88, 59374, 39, 1872, 1, 21, 1380, 2, 11, 1, 2933800883, 1, 16, 3171117007, 1, 17, 229, 1, 85, 2775, 11, 21, 21, 85, 1, 86, 996721205, 1, 87, 2487, 1, 88, 3911, 39, 1872, 5, 5, 9, 1, 4, 1525, 55, 1, 85, 36774, 12, 21, 21, 85, 1, 86, 3585821182, 1, 87, 33404, 1, 88, 57748, 39, 1872, 1, 21, 20052, 0, 92, 4, 0, 45, 92, 22, 92, 0, 6, 34, 90, 92, 33, 90, 90, 32, 90, 90, 1, 86, 144527848, 1, 91, 3366133224, 12, 91, 91, 86, 16, 91, 90, 91, 11, 86, 86, 91, 1, 87, 13930, 1, 88, 54929, 39, 1872, 1, 85, 46218, 11, 21, 21, 85, 1, 86, 2853626711, 1, 87, 10453, 1, 88, 40505, 39, 1872, 1, 85, 15161, 11, 21, 21, 85, 1, 86, 531011482, 1, 87, 23576, 1, 88, 11145, 39, 1872, 55, 1, 4, 1743, 1, 85, 63845, 12, 21, 21, 85, 1, 86, 778554373, 1, 87, 1927, 1, 88, 6728, 39, 1872, 1, 85, 31574, 11, 21, 21, 85, 1, 86, 1841158143, 1, 87, 7293, 1, 88, 22930, 39, 1872, 1, 21, 12534, 46, 5, 42, 86, 89, 3, 86, 87, 88, 58, 86, 47, 76, 5528, 3, 10, 0, 0, 4, 15, 0, 16, 30, 571479385, 5, 17, 16, 0, 18, 6, 464038654, 5, 19, 18, 0, 20, 31, 1691450989, 27, 21, 17, 20, 34, 78, 21, 33, 78, 78, 32, 78, 78, 1, 73, 4276234216, 1, 79, 855483625, 12, 79, 79, 73, 16, 79, 78, 79, 11, 73, 73, 79, 1, 74, 40653, 1, 75, 37211, 39, 3587, 0, 22, 32, 2942761608, 5, 19, 22, 26, 23, 17, 19, 34, 78, 23, 33, 78, 78, 32, 78, 78, 1, 73, 1175318111, 1, 79, 1112793748, 12, 79, 79, 73, 16, 79, 78, 79, 11, 73, 73, 79, 1, 74, 6723, 1, 75, 6315, 39, 3587, 1, 73, 2843788588, 1, 74, 39325, 1, 75, 46179, 39, 3587, 0, 24, 33, 3452820419, 11, 25, 19, 24, 5, 19, 25, 26, 26, 17, 19, 34, 78, 26, 33, 78, 78, 32, 78, 78, 1, 73, 2600861770, 1, 79, 2538341517, 12, 79, 79, 73, 16, 79, 78, 79, 11, 73, 73, 79, 1, 74, 46202, 1, 75, 44762, 39, 3587, 1, 73, 1543376953, 1, 74, 18690, 1, 75, 29694, 39, 3587, 0, 27, 34, 1687157215, 12, 28, 19, 27, 5, 19, 28, 26, 29, 17, 19, 34, 78, 29, 33, 78, 78, 1, 73, 121666807, 1, 79, 59144106, 12, 79, 79, 73, 13, 79, 78, 79, 11, 73, 73, 79, 1, 74, 56735, 1, 75, 27832, 39, 3587, 1, 73, 4163500210, 1, 74, 39923, 1, 75, 949, 39, 3587, 0, 30, 35, 3842168247, 12, 31, 19, 30, 5, 19, 31, 26, 32, 17, 19, 34, 78, 32, 33, 78, 78, 1, 73, 4167346069, 1, 79, 4104826060, 12, 79, 79, 73, 13, 79, 78, 79, 11, 73, 73, 79, 1, 74, 18707, 1, 75, 20019, 39, 3587, 1, 73, 2979415552, 1, 74, 13600, 1, 75, 16645, 39, 3587, 0, 33, 36, 477737094, 11, 34, 19, 33, 5, 19, 34, 26, 35, 17, 19, 34, 78, 35, 33, 78, 78, 1, 73, 897390586, 1, 79, 834874175, 12, 79, 79, 73, 13, 79, 78, 79, 11, 73, 73, 79, 1, 74, 15587, 1, 75, 46793, 39, 3587, 1, 73, 4221126994, 1, 74, 5636, 1, 75, 400, 39, 3587, 0, 36, 37, 1914508983, 12, 37, 19, 36, 5, 19, 37, 26, 38, 17, 19, 34, 78, 38, 33, 78, 78, 32, 78, 78, 1, 73, 396299373, 1, 79, 333776770, 12, 79, 79, 73, 16, 79, 78, 79, 11, 73, 73, 79, 1, 74, 65291, 1, 75, 17132, 39, 3587, 1, 73, 3973408308, 1, 74, 24892, 1, 75, 34237, 39, 3587, 0, 39, 38, 3171627982, 12, 40, 19, 39, 5, 19, 40, 26, 41, 17, 19, 34, 78, 41, 33, 78, 78, 1, 73, 182774004, 1, 79, 120249775, 12, 79, 79, 73, 13, 79, 79, 78, 11, 73, 73, 79, 1, 74, 611, 1, 75, 59585, 39, 3587, 1, 73, 1741012730, 1, 74, 19403, 1, 75, 22534, 39, 3587, 0, 42, 39, 1086883859, 11, 43, 19, 42, 5, 19, 43, 26, 44, 17, 19, 34, 78, 44, 33, 78, 78, 32, 78, 78, 1, 73, 1747099037, 1, 79, 1684572764, 12, 79, 79, 73, 16, 79, 78, 79, 11, 73, 73, 79, 1, 74, 9423, 1, 75, 10666, 39, 3587, 1, 73, 3229122769, 1, 74, 33411, 1, 75, 11946, 39, 3587, 0, 45, 40, 556797708, 12, 46, 19, 45, 5, 19, 46, 26, 47, 17, 19, 34, 78, 47, 33, 78, 78, 32, 78, 78, 1, 73, 2109132576, 1, 79, 2046612215, 12, 79, 79, 73, 16, 79, 78, 79, 11, 73, 73, 79, 1, 74, 27747, 1, 75, 42469, 39, 3587, 1, 73, 3832786062, 1, 74, 26624, 1, 75, 32147, 39, 3587, 0, 48, 41, 3657732308, 12, 49, 19, 48, 5, 19, 49, 26, 50, 17, 19, 34, 78, 50, 33, 78, 78, 1, 73, 1767330889, 1, 79, 1704808462, 12, 79, 79, 73, 13, 79, 79, 78, 11, 73, 73, 79, 1, 74, 33573, 1, 75, 3241, 39, 3587, 1, 73, 2073334613, 1, 74, 36231, 1, 75, 42490, 39, 3587, 0, 51, 42, 3399683983, 11, 52, 19, 51, 5, 19, 52, 26, 53, 17, 19, 34, 78, 53, 33, 78, 78, 1, 73, 3182830857, 1, 79, 3120310472, 12, 79, 79, 73, 13, 79, 79, 78, 11, 73, 73, 79, 1, 74, 18560, 1, 75, 11930, 39, 3587, 1, 73, 3045128875, 1, 74, 17443, 1, 75, 9822, 39, 3587, 0, 54, 43, 1211400967, 12, 55, 19, 54, 5, 19, 55, 26, 56, 17, 19, 34, 78, 56, 33, 78, 78, 32, 78, 78, 1, 73, 3715573905, 1, 79, 3653056486, 12, 79, 79, 73, 16, 79, 79, 78, 11, 73, 73, 79, 1, 74, 10340, 1, 75, 61953, 39, 3587, 1, 73, 592891182, 1, 74, 28041, 1, 75, 19434, 39, 3587, 0, 57, 44, 1982933380, 12, 58, 19, 57, 5, 19, 58, 26, 59, 17, 19, 34, 78, 59, 33, 78, 78, 1, 73, 1277438052, 1, 79, 1214911711, 12, 79, 79, 73, 13, 79, 78, 79, 11, 73, 73, 79, 1, 74, 19960, 1, 75, 29730, 39, 3587, 1, 73, 59488073, 1, 74, 21231, 1, 75, 36104, 39, 3587, 0, 60, 45, 654651774, 11, 61, 19, 60, 5, 19, 61, 26, 62, 17, 19, 34, 78, 62, 33, 78, 78, 1, 73, 2445367317, 1, 79, 2382842958, 12, 79, 79, 73, 13, 79, 78, 79, 11, 73, 73, 79, 1, 74, 29126, 1, 75, 10127, 39, 3587, 1, 73, 4003799190, 1, 74, 3976, 1, 75, 24818, 39, 3587, 0, 63, 46, 2587630286, 12, 64, 19, 63, 5, 19, 64, 26, 65, 17, 19, 34, 78, 65, 33, 78, 78, 1, 73, 138858526, 1, 79, 76338241, 12, 79, 79, 73, 13, 79, 79, 78, 11, 73, 73, 79, 1, 74, 9860, 1, 75, 17098, 39, 3587, 1, 73, 4137306052, 1, 74, 22980, 1, 75, 30905, 39, 3587, 0, 66, 47, 4150649578, 12, 67, 19, 66, 5, 19, 67, 26, 68, 17, 19, 34, 78, 68, 33, 78, 78, 1, 73, 2116526745, 1, 79, 2054008670, 12, 79, 79, 73, 13, 79, 78, 79, 11, 73, 73, 79, 1, 74, 37924, 1, 75, 15244, 39, 3587, 1, 73, 1505604913, 1, 74, 60024, 1, 75, 27713, 39, 3587, 0, 69, 48, 2751938669, 11, 70, 19, 69, 5, 19, 70, 26, 71, 17, 19, 34, 78, 71, 33, 78, 78, 32, 78, 78, 1, 73, 1494961286, 1, 79, 1432443763, 12, 79, 79, 73, 16, 79, 79, 78, 11, 73, 73, 79, 1, 74, 9152, 1, 75, 13993, 39, 3587, 1, 73, 2211454694, 1, 74, 18903, 1, 75, 41130, 39, 3587, 1, 73, 2721776586, 1, 74, 35337, 1, 75, 51933, 39, 3587, 45, 4, 4, 2, 2, 80, 49, 3247102209, 42, 6, 80, 1, 0, 1, 72, 18818, 12, 17, 17, 72, 1, 73, 4160829886, 1, 74, 61143, 1, 75, 36186, 39, 3587, 11, 80, 4, 9, 5, 4, 80, 1, 72, 5139, 12, 17, 17, 72, 1, 73, 1463510135, 1, 74, 19428, 1, 75, 3379, 39, 3587, 19, 80, 12, 14, 17, 81, 10, 80, 43, 9, 7, 8, 1, 81, 1, 72, 20322, 12, 17, 17, 72, 1, 73, 4105061186, 1, 74, 49284, 1, 75, 23272, 39, 3587, 5, 3, 6, 0, 80, 50, 904923811, 5, 4, 80, 1, 72, 27315, 12, 17, 17, 72, 1, 73, 2957038423, 1, 74, 54319, 1, 75, 37688, 39, 3587, 0, 80, 6, 464038654, 5, 5, 80, 1, 72, 45814, 11, 17, 17, 72, 1, 73, 4194434385, 1, 74, 1512, 1, 75, 27435, 39, 3587, 11, 80, 5, 13, 43, 12, 3, 11, 1, 80, 0, 14, 51, 2406067210, 1, 72, 6090, 11, 17, 17, 72, 1, 73, 319824871, 1, 74, 29199, 1, 75, 19630, 39, 3587, 0, 80, 52, 3295246026, 11, 81, 5, 80, 5, 5, 81, 1, 72, 29896, 11, 17, 17, 72, 1, 73, 2134986732, 1, 74, 21189, 1, 75, 8303, 39, 3587, 0, 80, 53, 2168139533, 8, 11, 3, 80, 0, 13, 2, 3954142846, 1, 72, 8051, 12, 17, 17, 72, 1, 73, 497903826, 1, 74, 17047, 1, 75, 35254, 39, 3587, 1, 72, 50213, 22, 72, 17, 72, 34, 78, 72, 33, 78, 78, 1, 73, 3224322131, 1, 79, 3717113856, 12, 79, 79, 73, 13, 79, 79, 78, 11, 73, 73, 79, 1, 74, 58966, 1, 75, 24609, 39, 3587, 1, 72, 44564, 12, 17, 17, 72, 1, 73, 333883808, 1, 74, 31198, 1, 75, 58963, 39, 3587, 1, 72, 50030, 12, 17, 17, 72, 1, 73, 2923568503, 1, 74, 24909, 1, 75, 44104, 39, 3587, 0, 80, 53, 2168139533, 8, 81, 3, 80, 43, 10, 3, 81, 1, 5, 1, 72, 37874, 11, 17, 17, 72, 1, 73, 101385721, 1, 74, 58762, 1, 75, 64500, 39, 3587, 1, 72, 61114, 22, 72, 17, 72, 34, 78, 72, 33, 78, 78, 32, 78, 78, 1, 73, 3154613441, 1, 79, 3647390910, 12, 79, 79, 73, 16, 79, 78, 79, 11, 73, 73, 79, 1, 74, 44558, 1, 75, 1061, 39, 3587, 1, 72, 3901, 11, 17, 17, 72, 1, 73, 1002524357, 1, 74, 24988, 1, 75, 18342, 39, 3587, 1, 72, 18382, 12, 17, 17, 72, 1, 73, 3992956198, 1, 74, 17649, 1, 75, 52128, 39, 3587, 2, 7, 54, 733816255, 0, 80, 55, 2635543199, 8, 8, 7, 80, 1, 72, 43874, 12, 17, 17, 72, 1, 73, 2170198285, 1, 74, 26991, 1, 75, 55160, 39, 3587, 1, 72, 7, 11, 17, 17, 72, 1, 73, 3874230044, 1, 74, 55432, 1, 75, 53237, 39, 3587, 1, 72, 35022, 22, 72, 17, 72, 34, 78, 72, 33, 78, 78, 1, 73, 3008293781, 1, 79, 3501038912, 12, 79, 79, 73, 13, 79, 79, 78, 11, 73, 73, 79, 1, 74, 22046, 1, 75, 54621, 39, 3587, 1, 72, 31029, 11, 17, 17, 72, 1, 73, 2264497638, 1, 74, 14391, 1, 75, 58814, 39, 3587, 1, 72, 5953, 11, 17, 17, 72, 1, 73, 3252665944, 1, 74, 6303, 1, 75, 54479, 39, 3587, 0, 80, 4, 0, 45, 80, 0, 80, 56, 411608100, 8, 81, 3, 80, 22, 80, 5, 81, 34, 78, 80, 33, 78, 78, 1, 73, 4234208508, 1, 79, 431952913, 12, 79, 79, 73, 13, 79, 78, 79, 11, 73, 73, 79, 1, 74, 44981, 1, 75, 16661, 39, 3587, 1, 72, 7085, 11, 17, 17, 72, 1, 73, 1041725201, 1, 74, 10398, 1, 75, 36516, 39, 3587, 1, 72, 3526, 11, 17, 17, 72, 1, 73, 4222779075, 1, 74, 44425, 1, 75, 28941, 39, 3587, 42, 73, 76, 3, 73, 74, 75, 0, 77, 57, 3975756945, 8, 73, 73, 77, 58, 73, 47, 86, 5699, 2, 8, 0, 0, 4, 20, 0, 21, 58, 3076830491, 5, 22, 21, 0, 23, 6, 464038654, 5, 24, 23, 0, 25, 59, 3767572577, 27, 26, 22, 25, 34, 87, 26, 33, 87, 87, 32, 87, 87, 1, 84, 2273933508, 1, 88, 1579703601, 12, 88, 88, 84, 16, 88, 88, 87, 11, 84, 84, 88, 1, 85, 527162100, 39, 5332, 0, 27, 60, 3638288145, 5, 24, 27, 26, 28, 22, 24, 34, 87, 28, 33, 87, 87, 32, 87, 87, 1, 84, 940752220, 1, 88, 3742968383, 12, 88, 88, 84, 16, 88, 88, 87, 11, 84, 84, 88, 1, 85, 1311982220, 39, 5332, 1, 84, 1097493578, 1, 85, 1043478440, 39, 5332, 0, 29, 61, 1413299743, 12, 30, 24, 29, 5, 24, 30, 26, 31, 22, 24, 34, 87, 31, 33, 87, 87, 32, 87, 87, 1, 84, 1965650843, 1, 88, 472849460, 12, 88, 88, 84, 16, 88, 87, 88, 11, 84, 84, 88, 1, 85, 251593195, 39, 5332, 1, 84, 3054024294, 1, 85, 3766099559, 39, 5332, 0, 32, 62, 2590780638, 11, 33, 24, 32, 5, 24, 33, 26, 34, 22, 24, 34, 87, 34, 33, 87, 87, 1, 84, 3905095281, 1, 88, 2412344578, 12, 88, 88, 84, 13, 88, 88, 87, 11, 84, 84, 88, 1, 85, 1156632905, 39, 5332, 1, 84, 26858267, 1, 85, 922261518, 39, 5332, 0, 35, 63, 806899558, 12, 36, 24, 35, 5, 24, 36, 26, 37, 22, 24, 34, 87, 37, 33, 87, 87, 32, 87, 87, 1, 84, 2599210510, 1, 88, 1106428345, 12, 88, 88, 84, 16, 88, 88, 87, 11, 84, 84, 88, 1, 85, 2060425349, 39, 5332, 1, 84, 4101523730, 1, 85, 4188323261, 39, 5332, 0, 38, 64, 540996091, 12, 39, 24, 38, 5, 24, 39, 26, 40, 22, 24, 34, 87, 40, 33, 87, 87, 32, 87, 87, 1, 84, 530592079, 1, 88, 3332802748, 12, 88, 88, 84, 16, 88, 88, 87, 11, 84, 84, 88, 1, 85, 2201515816, 39, 5332, 1, 84, 136307917, 1, 85, 2926995933, 39, 5332, 0, 41, 65, 2987054426, 12, 42, 24, 41, 5, 24, 42, 26, 43, 22, 24, 34, 87, 43, 33, 87, 87, 1, 84, 2800758129, 1, 88, 1308009222, 12, 88, 88, 84, 13, 88, 87, 88, 11, 84, 84, 88, 1, 85, 3437213634, 39, 5332, 1, 84, 1729750770, 1, 85, 1868846974, 39, 5332, 0, 44, 66, 1782648798, 11, 45, 24, 44, 5, 24, 45, 26, 46, 22, 24, 34, 87, 46, 33, 87, 87, 1, 84, 3835123249, 1, 88, 2342351326, 12, 88, 88, 84, 13, 88, 88, 87, 11, 84, 84, 88, 1, 85, 3437151706, 39, 5332, 1, 84, 3951659971, 1, 85, 2899118470, 39, 5332, 0, 47, 67, 357174050, 11, 48, 24, 47, 5, 24, 48, 26, 49, 22, 24, 34, 87, 49, 33, 87, 87, 1, 84, 2040154371, 1, 88, 547394152, 12, 88, 88, 84, 13, 88, 87, 88, 11, 84, 84, 88, 1, 85, 607728053, 39, 5332, 1, 84, 172943054, 1, 85, 3821395524, 39, 5332, 0, 50, 68, 1397035367, 12, 51, 24, 50, 5, 24, 51, 26, 52, 22, 24, 34, 87, 52, 33, 87, 87, 1, 84, 641249058, 1, 88, 3443477871, 12, 88, 88, 84, 13, 88, 88, 87, 11, 84, 84, 88, 1, 85, 2469367964, 39, 5332, 1, 84, 3488514598, 1, 85, 2007258215, 39, 5332, 0, 53, 69, 3981458885, 11, 54, 24, 53, 5, 24, 54, 26, 55, 22, 24, 34, 87, 55, 33, 87, 87, 32, 87, 87, 1, 84, 2237872712, 1, 88, 745104859, 12, 88, 88, 84, 16, 88, 88, 87, 11, 84, 84, 88, 1, 85, 1980704290, 39, 5332, 1, 84, 2117696316, 1, 85, 3707041578, 39, 5332, 0, 56, 70, 2794789816, 12, 57, 24, 56, 5, 24, 57, 26, 58, 22, 24, 34, 87, 58, 33, 87, 87, 32, 87, 87, 1, 84, 2460834643, 1, 88, 968098559, 12, 88, 88, 84, 16, 88, 87, 88, 11, 84, 84, 88, 1, 85, 118408905, 39, 5332, 1, 84, 1740748082, 1, 85, 1604830524, 39, 5332, 0, 59, 71, 606031711, 12, 60, 24, 59, 5, 24, 60, 26, 61, 22, 24, 34, 87, 61, 33, 87, 87, 32, 87, 87, 1, 84, 1403131674, 1, 88, 4205332877, 12, 88, 88, 84, 16, 88, 87, 88, 11, 84, 84, 88, 1, 85, 1270387191, 39, 5332, 1, 84, 1559719368, 1, 85, 422122166, 39, 5332, 0, 62, 72, 3951254113, 11, 63, 24, 62, 5, 24, 63, 26, 64, 22, 24, 34, 87, 64, 33, 87, 87, 1, 84, 3653020885, 1, 88, 2160267568, 12, 88, 88, 84, 13, 88, 87, 88, 11, 84, 84, 88, 1, 85, 2485817586, 39, 5332, 1, 84, 3147889968, 1, 85, 3529201906, 39, 5332, 0, 65, 73, 2876666649, 12, 66, 24, 65, 5, 24, 66, 26, 67, 22, 24, 34, 87, 67, 33, 87, 87, 32, 87, 87, 1, 84, 1549974288, 1, 88, 57255916, 12, 88, 88, 84, 16, 88, 87, 88, 11, 84, 84, 88, 1, 85, 2592486467, 39, 5332, 1, 84, 4014062593, 1, 85, 1868172288, 39, 5332, 0, 68, 74, 829978412, 12, 69, 24, 68, 5, 24, 69, 26, 70, 22, 24, 34, 87, 70, 33, 87, 87, 32, 87, 87, 1, 84, 1220502330, 1, 88, 4022690781, 12, 88, 88, 84, 16, 88, 87, 88, 11, 84, 84, 88, 1, 85, 177724766, 39, 5332, 1, 84, 1176185223, 1, 85, 2130462198, 39, 5332, 0, 71, 75, 2100089268, 11, 72, 24, 71, 5, 24, 72, 26, 73, 22, 24, 34, 87, 73, 33, 87, 87, 1, 84, 2778207696, 1, 88, 1285477483, 12, 88, 88, 84, 13, 88, 88, 87, 11, 84, 84, 88, 1, 85, 701011108, 39, 5332, 1, 84, 1953890858, 1, 85, 4227377918, 39, 5332, 0, 74, 76, 264133536, 11, 75, 24, 74, 5, 24, 75, 26, 76, 22, 24, 34, 87, 76, 33, 87, 87, 32, 87, 87, 1, 84, 2729548405, 1, 88, 1236778310, 12, 88, 88, 84, 16, 88, 88, 87, 11, 84, 84, 88, 1, 85, 2862022881, 39, 5332, 1, 84, 880974521, 1, 85, 3606175004, 39, 5332, 0, 77, 77, 212172472, 11, 78, 24, 77, 5, 24, 78, 26, 79, 22, 24, 34, 87, 79, 33, 87, 87, 32, 87, 87, 1, 84, 3848304703, 1, 88, 2355554218, 12, 88, 88, 84, 16, 88, 87, 88, 11, 84, 84, 88, 1, 85, 1895522850, 39, 5332, 1, 84, 2724723698, 1, 85, 2752618656, 39, 5332, 0, 80, 78, 59466251, 11, 81, 24, 80, 5, 24, 81, 26, 82, 22, 24, 34, 87, 82, 33, 87, 87, 32, 87, 87, 1, 84, 3241285324, 1, 88, 1748508681, 12, 88, 88, 84, 16, 88, 88, 87, 11, 84, 84, 88, 1, 85, 3498174817, 39, 5332, 1, 84, 813647452, 1, 85, 2838306966, 39, 5332, 1, 84, 1703662093, 1, 85, 1082983989, 39, 5332, 5, 89, 7, 0, 89, 2, 3954142846, 11, 19, 7, 89, 1, 83, 37281, 11, 22, 22, 83, 1, 84, 1693076133, 1, 85, 1072398029, 39, 5332, 45, 6, 1, 83, 37590, 22, 83, 22, 83, 34, 87, 83, 33, 87, 87, 1, 84, 447640929, 1, 88, 3783455140, 12, 88, 88, 84, 13, 88, 87, 88, 11, 84, 84, 88, 1, 85, 1788132360, 39, 5332, 1, 83, 41573, 12, 22, 22, 83, 1, 84, 3201768235, 1, 85, 2581090131, 39, 5332, 1, 83, 45802, 12, 22, 22, 83, 1, 84, 1872636865, 1, 85, 1251958761, 39, 5332, 16, 89, 12, 13, 5, 8, 89, 2, 14, 54, 733816255, 1, 83, 49463, 12, 22, 22, 83, 1, 84, 2470721191, 1, 85, 1850043087, 39, 5332, 5, 7, 19, 1, 83, 9212, 12, 22, 22, 83, 1, 84, 962873522, 1, 85, 342195418, 39, 5332, 5, 6, 18, 1, 83, 23917, 12, 22, 22, 83, 1, 84, 3837814786, 1, 85, 3217136682, 39, 5332, 1, 83, 31305, 22, 83, 22, 83, 34, 87, 83, 33, 87, 87, 1, 84, 3634618511, 1, 88, 2675452690, 12, 88, 88, 84, 13, 88, 87, 88, 11, 84, 84, 88, 1, 85, 3089537768, 39, 5332, 1, 83, 33477, 11, 22, 22, 83, 1, 84, 3079520546, 1, 85, 2458842442, 39, 5332, 1, 83, 33477, 11, 22, 22, 83, 1, 84, 2310346085, 1, 85, 1689667981, 39, 5332, 17, 89, 9, 10, 5, 1, 89, 0, 11, 79, 4180001374, 1, 83, 25218, 11, 22, 22, 83, 1, 84, 2599918174, 1, 85, 1979240070, 39, 5332, 0, 89, 6, 464038654, 5, 7, 89, 1, 83, 6741, 11, 22, 22, 83, 1, 84, 1850189109, 1, 85, 1229511005, 39, 5332, 18, 89, 17, 8, 43, 90, 14, 15, 1, 89, 11, 18, 6, 90, 1, 83, 28146, 11, 22, 22, 83, 1, 84, 1724793939, 1, 85, 1104115835, 39, 5332, 1, 83, 11843, 22, 83, 22, 83, 34, 87, 83, 33, 87, 87, 1, 84, 3572526346, 1, 88, 2613375043, 12, 88, 88, 84, 13, 88, 88, 87, 11, 84, 84, 88, 1, 85, 182729835, 39, 5332, 1, 83, 2553, 12, 22, 22, 83, 1, 84, 1041419006, 1, 85, 420740902, 39, 5332, 1, 83, 1739, 11, 22, 22, 83, 1, 84, 2727366779, 1, 85, 2106688675, 39, 5332, 22, 89, 7, 3, 34, 87, 89, 33, 87, 87, 1, 84, 884471595, 1, 88, 4220267298, 12, 88, 88, 84, 13, 88, 87, 88, 11, 84, 84, 88, 1, 85, 1197120190, 39, 5332, 1, 83, 5649, 12, 22, 22, 83, 1, 84, 126822071, 1, 85, 3801111263, 39, 5332, 1, 83, 2480, 11, 22, 22, 83, 1, 84, 1994943660, 1, 85, 1374265556, 39, 5332, 1, 83, 17141, 22, 83, 22, 83, 34, 87, 83, 33, 87, 87, 32, 87, 87, 1, 84, 3494728266, 1, 88, 2535542147, 12, 88, 88, 84, 16, 88, 88, 87, 11, 84, 84, 88, 1, 85, 1113190467, 39, 5332, 1, 83, 4360, 11, 22, 22, 83, 1, 84, 2368674555, 1, 85, 1747996451, 39, 5332, 1, 83, 4360, 11, 22, 22, 83, 1, 84, 1684538864, 1, 85, 1063860760, 39, 5332, 0, 89, 55, 2635543199, 8, 15, 14, 89, 0, 16, 53, 2168139533, 1, 83, 47717, 11, 22, 22, 83, 1, 84, 1602530725, 1, 85, 981852621, 39, 5332, 8, 89, 0, 16, 11, 90, 2, 7, 43, 17, 0, 89, 1, 90, 1, 83, 35250, 12, 22, 22, 83, 1, 84, 1009449541, 1, 85, 388771437, 39, 5332, 0, 89, 4, 0, 45, 89, 4, 5, 0, 89, 50, 904923811, 5, 6, 89, 1, 83, 15889, 11, 22, 22, 83, 1, 84, 3478576051, 1, 85, 2857897947, 39, 5332, 21, 89, 1, 11, 18, 12, 1, 89, 0, 13, 80, 1118291734, 1, 83, 10345, 12, 22, 22, 83, 1, 84, 2981570762, 1, 85, 2360892658, 39, 5332, 0, 89, 81, 3624238965, 11, 9, 1, 89, 0, 10, 6, 464038654, 1, 83, 12655, 12, 22, 22, 83, 1, 84, 2162996361, 1, 85, 1542318257, 39, 5332, 42, 84, 86, 2, 85, 84, 58, 84, 4, 4, 0, 6, 82, 3443926046, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 6, 464038654, 21, 8, 6, 7, 5, 5, 8, 18, 6, 0, 5, 0, 7, 6, 464038654, 21, 8, 6, 7, 5, 0, 8, 2, 6, 83, 2965691795, 0, 7, 84, 1426071625, 8, 8, 6, 7, 0, 7, 85, 3756260444, 43, 9, 6, 8, 2, 0, 7, 0, 6, 6, 464038654, 21, 7, 9, 6, 5, 0, 7, 0, 6, 86, 2578481828, 18, 7, 0, 6, 0, 6, 6, 464038654, 21, 8, 7, 6, 5, 0, 8, 35, 6, 0, 0, 7, 6, 464038654, 21, 8, 6, 7, 5, 0, 8, 0, 6, 87, 1917770209, 18, 7, 0, 6, 0, 6, 6, 464038654, 21, 8, 7, 6, 5, 0, 8, 0, 6, 88, 2237833085, 19, 7, 0, 6, 0, 6, 89, 2837511124, 21, 8, 0, 6, 17, 6, 7, 8, 0, 7, 6, 464038654, 21, 8, 6, 7, 5, 0, 8, 0, 6, 90, 550607678, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 6, 464038654, 21, 8, 6, 7, 5, 0, 8, 45, 0, 0, 6, 4, 0, 45, 6, 4, 4, 0, 6, 82, 3443926046, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 6, 464038654, 21, 8, 6, 7, 5, 5, 8, 0, 6, 82, 3443926046, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 6, 464038654, 21, 8, 6, 7, 5, 0, 8, 0, 6, 91, 3887321975, 12, 7, 0, 6, 0, 6, 6, 464038654, 21, 8, 7, 6, 5, 0, 8, 2, 6, 83, 2965691795, 0, 7, 84, 1426071625, 8, 8, 6, 7, 0, 7, 92, 3860911716, 43, 9, 6, 8, 2, 0, 7, 0, 6, 6, 464038654, 21, 7, 9, 6, 5, 0, 7, 11, 6, 0, 5, 0, 7, 6, 464038654, 21, 8, 6, 7, 5, 0, 8, 0, 6, 93, 3115657413, 11, 7, 0, 6, 0, 6, 6, 464038654, 21, 8, 7, 6, 5, 0, 8, 0, 6, 94, 1350850752, 12, 7, 0, 6, 0, 6, 6, 464038654, 21, 8, 7, 6, 5, 0, 8, 0, 6, 57, 3975756945, 49, 7, 1, 6, 0, 45, 7, 0, 6, 4, 0, 45, 6, 4, 3, 35, 4, 1, 0, 5, 6, 464038654, 21, 6, 4, 5, 5, 1, 6, 0, 4, 95, 3251060209, 11, 5, 1, 4, 0, 4, 6, 464038654, 21, 6, 5, 4, 5, 1, 6, 11, 4, 1, 0, 0, 5, 6, 464038654, 21, 6, 4, 5, 5, 1, 6, 0, 4, 96, 2632512983, 21, 5, 1, 4, 18, 4, 1, 5, 0, 5, 6, 464038654, 21, 6, 4, 5, 5, 1, 6, 2, 4, 83, 2965691795, 0, 5, 84, 1426071625, 8, 6, 4, 5, 0, 5, 97, 730090901, 43, 7, 4, 6, 2, 1, 5, 0, 4, 6, 464038654, 21, 5, 7, 4, 5, 1, 5, 45, 1, 0, 4, 4, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 17; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/qualification.json new file mode 100644 index 00000000..95612204 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/qualification.json @@ -0,0 +1,75 @@ +{ + "id": "exceptions--stringConcealingControlFlowFlatteningDispatcherConcealConstants--70", + "case": "exceptions", + "profile": "stringConcealingControlFlowFlatteningDispatcherConcealConstants", + "seed": 70, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true, + "concealConstants": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-concealed-constants/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/encoded.js new file mode 100644 index 00000000..97ba68f1 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"UrV64Tu7PAtbM6WS02ttZACF98mYoJ27+mUpvfprmdmkel19X9gS8NSdnPeuM0QDFiRP9GzL3qskN1a4mt1DM1QnG9Wcd8Sd/Y3Nf/saUzf1xfqRYA4zy5dBX48o0RIJ2bjwGqPbAt7W/O1etEE1wBvn5SoD0Iy9WF2TTl67y+/obsMA8oNO9x7jLyO1wiXEPo1+cog1TcZEOVRe9z/wluvSq0iBXNVQKuDrWD26NN+BINwwbMcRKaXWsze1PFrLdwU8dYFK7ngZqJAGA3DRdNJfcwj8THQv3DirW1dXpoAo+bbqo5qQfywbfkmcGzdn/saYNc8uGEhX3u823Jjw4fzvZl/IenTjlw95xDb/lgGxZy1SfJ0yLlvnpzykhwL3zpjq89Y6au+JxxhO69MhY1HoHsh0b6Z1T+NsRpfT5m2fo3iOaW5bufeyzcQhxKqcVcFJRte6ewPj2jYRhvn+j8uoN/+bPsqtAg54gYL6hLYPuvLF2HIy37IjJKenEgCNgQXgmuYO4ru797KmMiKhby3yMkamzT46o1aA3kqpe86QxRdaks5ienDmW6pDPlQ8Ja6svJBgtCbl5vp64Y/CG92GzQrCkRGo1oMeBZfDXA/ijEI6OndSzMT5sE0h3wYcqReTjy0pkzjHsukdKfMLQhxTYFt/QX1IRoINoXvX+24otpoOEp1Zahrw+uuvhOCChKPaz708d4IjeLzIzpRMbmJojigw9UR65K+H1Q0ZDlgcrzee+PEsFh4xAU+uD/eRRMzHGOHaIB36vqF+7O0BKyPtBXtiDHtjLOcGRJeEAHsE9Y9790z4Jj6WzvK+AVp5f5UeTmmSP+Ejx5tKFWoWIAzfdk6ig4UWl8zti7vdYIM8mXRA6sPTA4LhNiU0a3tAlPmg4K8vOrU56DgmnilMrCwyA/+8z67QjqnH70coKBm3ACTCKiTQy1LEdPDbQiqrhn0edcJ7Y8JLF/EVMOD05hLJ2XS7DE3tEqwKP0DDsXoOPIVK7VfMfYdIbrDGXCvdjrRTlG5Ls/Oy3rsdKijGhKRblloSp8KexEXx9caTZvQS8KmuE7akEe3F3OepoIbjHUNUhUVJjiLtm48hV6P5p87LIVOIehE9rTUpzZ4fUgTr1CqPtqL+PK2B5jQpYXM5biU=", /* 1 */"window", /* 2 */1, /* 3 */4, /* 4 */undefined, /* 5 */62479, /* 6 */0, /* 7 */59378, /* 8 */15359, /* 9 */431, /* 10 */37405, /* 11 */30655, /* 12 */7264, /* 13 */3478, /* 14 */43725, /* 15 */30746, /* 16 */22506, /* 17 */51053, /* 18 */55617, /* 19 */49166, /* 20 */9881, /* 21 */1959, /* 22 */10051, /* 23 */42261, /* 24 */18535, /* 25 */26335, /* 26 */44010, /* 27 */33102, /* 28 */4048, /* 29 */"Error", /* 30 */true, /* 31 */58590, /* 32 */31333, /* 33 */10109, /* 34 */49751, /* 35 */5204, /* 36 */3934, /* 37 */15703, /* 38 */38083, /* 39 */1274, /* 40 */32224, /* 41 */19587, /* 42 */40967, /* 43 */42295, /* 44 */4852, /* 45 */47751, /* 46 */29325, /* 47 */"length", /* 48 */"charCodeAt", /* 49 */2, /* 50 */"String", /* 51 */"fromCharCode", /* 52 */8, /* 53 */"atob", /* 54 */"", /* 55 */"z7yqc", /* 56 */54869, /* 57 */2019, /* 58 */60866, /* 59 */14610, /* 60 */2831, /* 61 */16216, /* 62 */1059, /* 63 */4482, /* 64 */3580, /* 65 */44808, /* 66 */47039, /* 67 */24464, /* 68 */20867, /* 69 */28940, /* 70 */15190, /* 71 */10048, /* 72 */14080, /* 73 */46048, /* 74 */21238, /* 75 */23230, /* 76 */59633, /* 77 */65535, /* 78 */2654435769, /* 79 */13, /* 80 */16, /* 81 */2984753255, /* 82 */30, /* 83 */"Math", /* 84 */"imul", /* 85 */1177571661, /* 86 */2598056223, /* 87 */2267017141, /* 88 */1984206505, /* 89 */2124058793, /* 90 */21, /* 91 */2571413032, /* 92 */3511614151, /* 93 */3882636965]; +var BYTECODE = [47, 6, 2017, 1, 73, 0, 0, 0, 7, 0, 0, 42, 5, 6, 1, 7, 47, 8, 3332, 4, 93, 0, 0, 4, 1, 47, 12, 120, 1, 96, 1, 0, 1, 5, 5, 2, 12, 2, 12, 1, 0, 1, 9, 2114760142, 1, 10, 185, 1, 11, 11, 42, 3, 8, 4, 5, 9, 10, 11, 0, 13, 2, 0, 32, 14, 13, 42, 13, 2, 1, 14, 0, 14, 3, 0, 42, 15, 2, 1, 14, 2, 14, 1, 0, 1, 9, 1135751136, 1, 10, 197, 1, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 8, 16, 14, 4, 48, 14, 3, 13, 15, 16, 9, 12, 3, 14, 0, 12, 4, 0, 45, 12, 47, 92, 5094, 3, 10, 0, 0, 4, 19, 0, 20, 5, 0, 5, 21, 20, 0, 22, 6, 0, 5, 23, 22, 0, 24, 7, 0, 27, 25, 21, 24, 34, 93, 25, 33, 93, 93, 1, 89, 1634785495, 1, 94, 2757368938, 12, 94, 94, 89, 13, 94, 94, 93, 11, 89, 89, 94, 1, 90, 57739, 1, 91, 26686, 39, 2008, 0, 26, 8, 0, 5, 23, 26, 26, 27, 21, 23, 34, 93, 27, 33, 93, 93, 1, 89, 91608710, 1, 94, 309478333, 12, 94, 94, 89, 13, 94, 94, 93, 11, 89, 89, 94, 1, 90, 850, 1, 91, 65424, 39, 2008, 1, 89, 4017671625, 1, 90, 39217, 1, 91, 37864, 39, 2008, 0, 28, 9, 0, 11, 29, 23, 28, 5, 23, 29, 26, 30, 21, 23, 34, 93, 30, 33, 93, 93, 32, 93, 93, 1, 89, 3615229108, 1, 94, 3899645793, 12, 94, 94, 89, 16, 94, 93, 94, 11, 89, 89, 94, 1, 90, 23630, 1, 91, 64964, 39, 2008, 1, 89, 3125523877, 1, 90, 19854, 1, 91, 4171, 39, 2008, 0, 31, 10, 0, 11, 32, 23, 31, 5, 23, 32, 26, 33, 21, 23, 34, 93, 33, 33, 93, 93, 1, 89, 2000066059, 1, 94, 2052747206, 12, 94, 94, 89, 13, 94, 93, 94, 11, 89, 89, 94, 1, 90, 51591, 1, 91, 34990, 39, 2008, 1, 89, 922238064, 1, 90, 30577, 1, 91, 2730, 39, 2008, 0, 34, 11, 0, 12, 35, 23, 34, 5, 23, 35, 26, 36, 21, 23, 34, 93, 36, 33, 93, 93, 1, 89, 911911676, 1, 94, 2740330642, 12, 94, 94, 89, 13, 94, 93, 94, 11, 89, 89, 94, 1, 90, 60114, 1, 91, 18178, 39, 2008, 1, 89, 1217496363, 1, 90, 36562, 1, 91, 37505, 39, 2008, 0, 37, 12, 0, 12, 38, 23, 37, 5, 23, 38, 26, 39, 21, 23, 34, 93, 39, 33, 93, 93, 32, 93, 93, 1, 89, 1670232710, 1, 94, 1854027181, 12, 94, 94, 89, 16, 94, 94, 93, 11, 89, 89, 94, 1, 90, 2281, 1, 91, 24068, 39, 2008, 1, 89, 3208782805, 1, 90, 63413, 1, 91, 35299, 39, 2008, 0, 40, 13, 0, 11, 41, 23, 40, 5, 23, 41, 26, 42, 21, 23, 34, 93, 42, 33, 93, 93, 32, 93, 93, 1, 89, 2467728345, 1, 94, 2786393840, 12, 94, 94, 89, 16, 94, 93, 94, 11, 89, 89, 94, 1, 90, 11988, 1, 91, 64253, 39, 2008, 1, 89, 2722534732, 1, 90, 36142, 1, 91, 53849, 39, 2008, 0, 43, 14, 0, 11, 44, 23, 43, 5, 23, 44, 26, 45, 21, 23, 34, 93, 45, 33, 93, 93, 32, 93, 93, 1, 89, 3899142711, 1, 94, 4151282415, 12, 94, 94, 89, 16, 94, 94, 93, 11, 89, 89, 94, 1, 90, 35206, 1, 91, 32389, 39, 2008, 1, 89, 2583845925, 1, 90, 19329, 1, 91, 7854, 39, 2008, 0, 46, 15, 0, 12, 47, 23, 46, 5, 23, 47, 26, 48, 21, 23, 34, 93, 48, 33, 93, 93, 1, 89, 364614282, 1, 94, 549202385, 12, 94, 94, 89, 13, 94, 93, 94, 11, 89, 89, 94, 1, 90, 11864, 1, 91, 56902, 39, 2008, 1, 89, 280254107, 1, 90, 33559, 1, 91, 26728, 39, 2008, 0, 49, 16, 0, 11, 50, 23, 49, 5, 23, 50, 26, 51, 21, 23, 34, 93, 51, 33, 93, 93, 1, 89, 1096412560, 1, 94, 1281189703, 12, 94, 94, 89, 13, 94, 93, 94, 11, 89, 89, 94, 1, 90, 52658, 1, 91, 48215, 39, 2008, 1, 89, 3292203072, 1, 90, 30219, 1, 91, 2907, 39, 2008, 0, 52, 17, 0, 12, 53, 23, 52, 5, 23, 53, 26, 54, 21, 23, 34, 93, 54, 33, 93, 93, 32, 93, 93, 1, 89, 3481031684, 1, 94, 3263094585, 12, 94, 94, 89, 16, 94, 93, 94, 11, 89, 89, 94, 1, 90, 28880, 1, 91, 17091, 39, 2008, 1, 89, 1242864625, 1, 90, 14801, 1, 91, 10045, 39, 2008, 0, 55, 18, 0, 11, 56, 23, 55, 5, 23, 56, 26, 57, 21, 23, 34, 93, 57, 33, 93, 93, 1, 89, 2529841697, 1, 94, 2848515416, 12, 94, 94, 89, 13, 94, 94, 93, 11, 89, 89, 94, 1, 90, 30423, 1, 91, 12293, 39, 2008, 1, 89, 3539458799, 1, 90, 33645, 1, 91, 19575, 39, 2008, 0, 58, 19, 0, 12, 59, 23, 58, 5, 23, 59, 26, 60, 21, 23, 34, 93, 60, 33, 93, 93, 1, 89, 3549531953, 1, 94, 143799171, 12, 94, 94, 89, 13, 94, 93, 94, 11, 89, 89, 94, 1, 90, 9563, 1, 91, 27614, 39, 2008, 1, 89, 3518618210, 1, 90, 38279, 1, 91, 25021, 39, 2008, 0, 61, 20, 0, 11, 62, 23, 61, 5, 23, 62, 26, 63, 21, 23, 34, 93, 63, 33, 93, 93, 1, 89, 2128643615, 1, 94, 1910706377, 12, 94, 94, 89, 13, 94, 93, 94, 11, 89, 89, 94, 1, 90, 14857, 1, 91, 3327, 39, 2008, 1, 89, 1688423265, 1, 90, 49690, 1, 91, 7250, 39, 2008, 0, 64, 21, 0, 11, 65, 23, 64, 5, 23, 65, 26, 66, 21, 23, 34, 93, 66, 33, 93, 93, 32, 93, 93, 1, 89, 1941979858, 1, 94, 2763976057, 12, 94, 94, 89, 16, 94, 94, 93, 11, 89, 89, 94, 1, 90, 24587, 1, 91, 11745, 39, 2008, 1, 89, 2206045937, 1, 90, 4862, 1, 91, 22267, 39, 2008, 0, 67, 22, 0, 12, 68, 23, 67, 5, 23, 68, 26, 69, 21, 23, 34, 93, 69, 33, 93, 93, 1, 89, 3535908044, 1, 94, 3351116867, 12, 94, 94, 89, 13, 94, 93, 94, 11, 89, 89, 94, 1, 90, 6881, 1, 91, 16656, 39, 2008, 1, 89, 414296844, 1, 90, 13772, 1, 91, 63497, 39, 2008, 0, 70, 23, 0, 11, 71, 23, 70, 5, 23, 71, 26, 72, 21, 23, 34, 93, 72, 33, 93, 93, 32, 93, 93, 1, 89, 3589021585, 1, 94, 3673130584, 12, 94, 94, 89, 16, 94, 93, 94, 11, 89, 89, 94, 1, 90, 34334, 1, 91, 43519, 39, 2008, 1, 89, 4170481066, 1, 90, 54880, 1, 91, 26281, 39, 2008, 0, 73, 24, 0, 12, 74, 23, 73, 5, 23, 74, 26, 75, 21, 23, 34, 93, 75, 33, 93, 93, 32, 93, 93, 1, 89, 4057744652, 1, 94, 3671792195, 12, 94, 94, 89, 16, 94, 93, 94, 11, 89, 89, 94, 1, 90, 24384, 1, 91, 34265, 39, 2008, 1, 89, 3876998001, 1, 90, 20469, 1, 91, 26752, 39, 2008, 0, 76, 25, 0, 11, 77, 23, 76, 5, 23, 77, 26, 78, 21, 23, 34, 93, 78, 33, 93, 93, 1, 89, 4026333845, 1, 94, 3841626446, 12, 94, 94, 89, 13, 94, 94, 93, 11, 89, 89, 94, 1, 90, 39463, 1, 91, 4520, 39, 2008, 1, 89, 2137916640, 1, 90, 10433, 1, 91, 36830, 39, 2008, 0, 79, 26, 0, 12, 80, 23, 79, 5, 23, 80, 26, 81, 21, 23, 34, 93, 81, 33, 93, 93, 32, 93, 93, 1, 89, 2759398630, 1, 94, 2574933019, 12, 94, 94, 89, 16, 94, 93, 94, 11, 89, 89, 94, 1, 90, 38596, 1, 91, 43390, 39, 2008, 1, 89, 3438074401, 1, 90, 44383, 1, 91, 2379, 39, 2008, 0, 82, 27, 0, 11, 83, 23, 82, 5, 23, 83, 26, 84, 21, 23, 34, 93, 84, 33, 93, 93, 32, 93, 93, 1, 89, 1851571557, 1, 94, 20072987, 12, 94, 94, 89, 16, 94, 94, 93, 11, 89, 89, 94, 1, 90, 47511, 1, 91, 13396, 39, 2008, 1, 89, 1653484214, 1, 90, 63431, 1, 91, 10986, 39, 2008, 0, 85, 28, 0, 11, 86, 23, 85, 5, 23, 86, 26, 87, 21, 23, 34, 93, 87, 33, 93, 93, 32, 93, 93, 1, 89, 424692334, 1, 94, 642969903, 12, 94, 94, 89, 16, 94, 94, 93, 11, 89, 89, 94, 1, 90, 8729, 1, 91, 54075, 39, 2008, 1, 89, 4261872598, 1, 90, 58310, 1, 91, 52619, 39, 2008, 1, 89, 934104696, 1, 90, 49955, 1, 91, 7222, 39, 2008, 1, 21, 50579, 45, 5, 59, 1551, 4, 5, 1865, 54, 1715, 3, 0, 6, 6, 0, 1, 88, 7800, 11, 21, 21, 88, 1, 89, 2499567879, 1, 90, 64914, 1, 91, 31463, 39, 2008, 22, 95, 0, 6, 34, 93, 95, 33, 93, 93, 32, 93, 93, 1, 89, 881435699, 1, 94, 2354392014, 12, 94, 94, 89, 16, 94, 94, 93, 11, 89, 89, 94, 1, 90, 62105, 1, 91, 21244, 39, 2008, 1, 88, 38947, 12, 21, 21, 88, 1, 89, 3936944778, 1, 90, 7953, 1, 91, 41266, 39, 2008, 1, 88, 58301, 12, 21, 21, 88, 1, 89, 3908290129, 1, 90, 22744, 1, 91, 44799, 39, 2008, 1, 21, 15790, 2, 11, 1, 0, 1, 16, 1135751136, 1, 17, 197, 1, 88, 514, 12, 21, 21, 88, 1, 89, 87612985, 1, 90, 28896, 1, 91, 51874, 39, 2008, 1, 88, 50410, 22, 88, 21, 88, 34, 93, 88, 33, 93, 93, 32, 93, 93, 1, 89, 2065263296, 1, 94, 1667617689, 12, 94, 94, 89, 16, 94, 94, 93, 11, 89, 89, 94, 1, 90, 18095, 1, 91, 44956, 39, 2008, 1, 88, 2723, 11, 21, 21, 88, 1, 89, 668898384, 1, 90, 36571, 1, 91, 60421, 39, 2008, 1, 88, 16398, 11, 21, 21, 88, 1, 89, 3383163142, 1, 90, 5549, 1, 91, 36428, 39, 2008, 42, 10, 15, 4, 14, 16, 17, 18, 8, 95, 3, 10, 5, 5, 95, 1, 88, 3933, 12, 21, 21, 88, 1, 89, 581976292, 1, 90, 56655, 1, 91, 59707, 39, 2008, 55, 1, 4, 1925, 1, 88, 5687, 12, 21, 21, 88, 1, 89, 1373784994, 1, 90, 50697, 1, 91, 1545, 39, 2008, 1, 21, 58803, 1, 16, 1028113336, 1, 17, 220, 1, 18, 7, 1, 88, 43444, 12, 21, 21, 88, 1, 89, 3924206763, 1, 90, 47478, 1, 91, 44557, 39, 2008, 55, 1, 88, 31718, 12, 21, 21, 88, 1, 89, 2600154998, 1, 90, 10813, 1, 91, 28896, 39, 2008, 2, 7, 29, 0, 1, 16, 995264652, 1, 17, 207, 1, 88, 28547, 11, 21, 21, 88, 1, 89, 2160001465, 1, 90, 7264, 1, 91, 22308, 39, 2008, 5, 5, 9, 1, 4, 1441, 55, 1, 88, 1687, 12, 21, 21, 88, 1, 89, 4194982135, 1, 90, 62882, 1, 91, 53713, 39, 2008, 1, 88, 41969, 11, 21, 21, 88, 1, 89, 1049579486, 1, 90, 49733, 1, 91, 5460, 39, 2008, 3, 14, 0, 47, 15, 3332, 4, 93, 0, 0, 4, 2, 1, 88, 8792, 12, 21, 21, 88, 1, 89, 3823477826, 1, 90, 48873, 1, 91, 43022, 39, 2008, 1, 21, 54239, 46, 5, 1, 4, 1976, 55, 1, 88, 4364, 11, 21, 21, 88, 1, 89, 60213375, 1, 90, 17706, 1, 91, 51292, 39, 2008, 1, 18, 8, 42, 12, 15, 4, 14, 16, 17, 18, 0, 13, 30, 0, 1, 88, 39351, 11, 21, 21, 88, 1, 89, 2141303313, 1, 90, 44184, 1, 91, 21578, 39, 2008, 1, 21, 35152, 0, 95, 4, 0, 45, 95, 9, 11, 12, 13, 58, 4, 1, 88, 388, 12, 21, 21, 88, 1, 89, 1555243654, 1, 90, 16173, 1, 91, 13112, 39, 2008, 1, 18, 8, 42, 8, 15, 4, 14, 16, 17, 18, 44, 95, 7, 1, 8, 46, 95, 1, 21, 9637, 45, 5, 0, 95, 2, 0, 11, 9, 0, 95, 55, 1, 88, 5063, 12, 21, 21, 88, 1, 89, 1363615993, 1, 90, 15776, 1, 91, 1708, 39, 2008, 42, 89, 92, 3, 89, 90, 91, 58, 89, 47, 67, 5237, 3, 10, 0, 0, 4, 15, 0, 16, 31, 0, 5, 17, 16, 0, 18, 6, 0, 5, 19, 18, 0, 20, 32, 0, 27, 21, 17, 20, 34, 69, 21, 33, 69, 69, 32, 69, 69, 1, 64, 1968757432, 1, 70, 553360557, 12, 70, 70, 64, 16, 70, 70, 69, 11, 64, 64, 70, 1, 65, 33726, 1, 66, 59176, 39, 3315, 0, 22, 33, 0, 5, 19, 22, 26, 23, 17, 19, 34, 69, 23, 33, 69, 69, 1, 64, 2356054407, 1, 70, 4002817488, 12, 70, 70, 64, 13, 70, 70, 69, 11, 64, 64, 70, 1, 65, 50882, 1, 66, 43047, 39, 3315, 1, 64, 2088537410, 1, 65, 22526, 1, 66, 2730, 39, 3315, 0, 24, 34, 0, 11, 25, 19, 24, 5, 19, 25, 26, 26, 17, 19, 34, 69, 26, 33, 69, 69, 32, 69, 69, 1, 64, 297273617, 1, 70, 1880153356, 12, 70, 70, 64, 16, 70, 70, 69, 11, 64, 64, 70, 1, 65, 48399, 1, 66, 10587, 39, 3315, 1, 64, 4231770231, 1, 65, 46036, 1, 66, 44171, 39, 3315, 0, 27, 35, 0, 12, 28, 19, 27, 5, 19, 28, 26, 29, 17, 19, 34, 69, 29, 33, 69, 69, 32, 69, 69, 1, 64, 2151909279, 1, 70, 2786777941, 12, 70, 70, 64, 16, 70, 69, 70, 11, 64, 64, 70, 1, 65, 36833, 1, 66, 56318, 39, 3315, 1, 64, 3881813827, 1, 65, 8180, 1, 66, 8332, 39, 3315, 0, 30, 36, 0, 11, 31, 19, 30, 5, 19, 31, 26, 32, 17, 19, 34, 69, 32, 33, 69, 69, 1, 64, 2871561081, 1, 70, 3369084278, 12, 70, 70, 64, 13, 70, 70, 69, 11, 64, 64, 70, 1, 65, 62422, 1, 66, 6884, 39, 3315, 1, 64, 873787444, 1, 65, 19447, 1, 66, 8876, 39, 3315, 0, 33, 37, 0, 12, 34, 19, 33, 5, 19, 34, 26, 35, 17, 19, 34, 69, 35, 33, 69, 69, 1, 64, 553881935, 1, 70, 526088612, 12, 70, 70, 64, 13, 70, 69, 70, 11, 64, 64, 70, 1, 65, 20261, 1, 66, 14104, 39, 3315, 1, 64, 85781302, 1, 65, 54475, 1, 66, 2951, 39, 3315, 0, 36, 38, 0, 12, 37, 19, 36, 5, 19, 37, 26, 38, 17, 19, 34, 69, 38, 33, 69, 69, 1, 64, 1411076994, 1, 70, 3064123300, 12, 70, 70, 64, 13, 70, 69, 70, 11, 64, 64, 70, 1, 65, 3498, 1, 66, 14384, 39, 3315, 1, 64, 2001593360, 1, 65, 41928, 1, 66, 46306, 39, 3315, 0, 39, 39, 0, 12, 40, 19, 39, 5, 19, 40, 26, 41, 17, 19, 34, 69, 41, 33, 69, 69, 1, 64, 2795316489, 1, 70, 2152125985, 12, 70, 70, 64, 13, 70, 69, 70, 11, 64, 64, 70, 1, 65, 54801, 1, 66, 25831, 39, 3315, 1, 64, 3210343143, 1, 65, 174, 1, 66, 61202, 39, 3315, 0, 42, 40, 0, 11, 43, 19, 42, 5, 19, 43, 26, 44, 17, 19, 34, 69, 44, 33, 69, 69, 32, 69, 69, 1, 64, 2937455141, 1, 70, 1299823319, 12, 70, 70, 64, 16, 70, 69, 70, 11, 64, 64, 70, 1, 65, 38179, 1, 66, 18946, 39, 3315, 1, 64, 1226369308, 1, 65, 25758, 1, 66, 35725, 39, 3315, 0, 45, 41, 0, 11, 46, 19, 45, 5, 19, 46, 26, 47, 17, 19, 34, 69, 47, 33, 69, 69, 1, 64, 1023119608, 1, 70, 1515338748, 12, 70, 70, 64, 13, 70, 70, 69, 11, 64, 64, 70, 1, 65, 61433, 1, 66, 2008, 39, 3315, 1, 64, 2878706361, 1, 65, 29552, 1, 66, 14642, 39, 3315, 0, 48, 42, 0, 12, 49, 19, 48, 5, 19, 49, 26, 50, 17, 19, 34, 69, 50, 33, 69, 69, 32, 69, 69, 1, 64, 4213264567, 1, 70, 3573074429, 12, 70, 70, 64, 16, 70, 70, 69, 11, 64, 64, 70, 1, 65, 36327, 1, 66, 42204, 39, 3315, 1, 64, 3535363, 1, 65, 45249, 1, 66, 42291, 39, 3315, 0, 51, 43, 0, 11, 52, 19, 51, 5, 19, 52, 26, 53, 17, 19, 34, 69, 53, 33, 69, 69, 1, 64, 3506361767, 1, 70, 2994830775, 12, 70, 70, 64, 13, 70, 69, 70, 11, 64, 64, 70, 1, 65, 29826, 1, 66, 25842, 39, 3315, 1, 64, 1273480994, 1, 65, 42911, 1, 66, 43670, 39, 3315, 0, 54, 44, 0, 12, 55, 19, 54, 5, 19, 55, 26, 56, 17, 19, 34, 69, 56, 33, 69, 69, 1, 64, 2081538748, 1, 70, 1523105750, 12, 70, 70, 64, 13, 70, 69, 70, 11, 64, 64, 70, 1, 65, 8704, 1, 66, 30282, 39, 3315, 1, 64, 780640038, 1, 65, 32631, 1, 66, 9158, 39, 3315, 0, 57, 45, 0, 12, 58, 19, 57, 5, 19, 58, 26, 59, 17, 19, 34, 69, 59, 33, 69, 69, 1, 64, 228212817, 1, 70, 4011989118, 12, 70, 70, 64, 13, 70, 69, 70, 11, 64, 64, 70, 1, 65, 58812, 1, 66, 28144, 39, 3315, 1, 64, 2180610239, 1, 65, 56388, 1, 66, 14425, 39, 3315, 0, 60, 46, 0, 11, 61, 19, 60, 5, 19, 61, 26, 62, 17, 19, 34, 69, 62, 33, 69, 69, 32, 69, 69, 1, 64, 538569919, 1, 70, 108442925, 12, 70, 70, 64, 16, 70, 70, 69, 11, 64, 64, 70, 1, 65, 17956, 1, 66, 41757, 39, 3315, 1, 64, 3329575281, 1, 65, 6334, 1, 66, 38038, 39, 3315, 1, 64, 1765167885, 1, 65, 36534, 1, 66, 27040, 39, 3315, 0, 71, 47, 0, 8, 72, 3, 71, 22, 71, 5, 72, 34, 69, 71, 33, 69, 69, 1, 64, 958429822, 1, 70, 3420484988, 12, 70, 70, 64, 13, 70, 70, 69, 11, 64, 64, 70, 1, 65, 63360, 1, 66, 63184, 39, 3315, 1, 63, 50590, 12, 17, 17, 63, 1, 64, 1226004445, 1, 65, 43587, 1, 66, 18821, 39, 3315, 1, 63, 44547, 12, 17, 17, 63, 1, 64, 4265175723, 1, 65, 8223, 1, 66, 65199, 39, 3315, 1, 63, 16973, 12, 17, 17, 63, 1, 64, 1258454359, 1, 65, 33496, 1, 66, 19348, 39, 3315, 19, 71, 12, 14, 17, 72, 10, 71, 43, 9, 7, 8, 1, 72, 1, 63, 4852, 12, 17, 17, 63, 1, 64, 3142763608, 1, 65, 52103, 1, 66, 48068, 39, 3315, 0, 71, 48, 0, 8, 72, 3, 71, 43, 10, 3, 72, 1, 5, 1, 63, 9496, 11, 17, 17, 63, 1, 64, 899364140, 1, 65, 46138, 1, 66, 13581, 39, 3315, 0, 71, 6, 0, 5, 5, 71, 1, 63, 18902, 11, 17, 17, 63, 1, 64, 3514031873, 1, 65, 47871, 1, 66, 53733, 39, 3315, 0, 71, 49, 0, 11, 72, 5, 71, 5, 5, 72, 1, 63, 49852, 11, 17, 17, 63, 1, 64, 3371378846, 1, 65, 21728, 1, 66, 51301, 39, 3315, 2, 7, 50, 0, 0, 71, 51, 0, 8, 8, 7, 71, 1, 63, 23282, 11, 17, 17, 63, 1, 64, 160635078, 1, 65, 42456, 1, 66, 2309, 39, 3315, 11, 71, 4, 9, 5, 4, 71, 1, 63, 47013, 12, 17, 17, 63, 1, 64, 2429386318, 1, 65, 23054, 1, 66, 36955, 39, 3315, 0, 71, 48, 0, 8, 11, 3, 71, 0, 13, 2, 0, 1, 63, 28513, 12, 17, 17, 63, 1, 64, 3340140358, 1, 65, 3293, 1, 66, 51072, 39, 3315, 11, 71, 5, 13, 43, 12, 3, 11, 1, 71, 0, 14, 52, 0, 1, 63, 42295, 11, 17, 17, 63, 1, 64, 2443401093, 1, 65, 29355, 1, 66, 37173, 39, 3315, 4, 2, 2, 71, 53, 0, 42, 6, 71, 1, 0, 1, 63, 3249, 12, 17, 17, 63, 1, 64, 2674241634, 1, 65, 39818, 1, 66, 40947, 39, 3315, 45, 4, 5, 3, 6, 0, 71, 54, 0, 5, 4, 71, 1, 63, 19587, 12, 17, 17, 63, 1, 64, 2384472019, 1, 65, 7550, 1, 66, 36534, 39, 3315, 0, 71, 4, 0, 45, 71, 42, 64, 67, 3, 64, 65, 66, 0, 68, 55, 0, 8, 64, 64, 68, 58, 64, 47, 87, 5370, 3, 10, 0, 0, 4, 20, 0, 21, 56, 0, 5, 22, 21, 0, 23, 6, 0, 5, 24, 23, 0, 25, 57, 0, 27, 26, 22, 25, 34, 89, 26, 33, 89, 89, 32, 89, 89, 1, 84, 3051656986, 1, 90, 180108693, 12, 90, 90, 84, 16, 90, 90, 89, 11, 84, 84, 90, 1, 85, 37500, 1, 86, 31605, 39, 5078, 0, 27, 58, 0, 5, 24, 27, 26, 28, 22, 24, 34, 89, 28, 33, 89, 89, 1, 84, 3739444999, 1, 90, 775604484, 12, 90, 90, 84, 13, 90, 89, 90, 11, 84, 84, 90, 1, 85, 254, 1, 86, 23479, 39, 5078, 1, 84, 4251840681, 1, 85, 30432, 1, 86, 14691, 39, 5078, 0, 29, 59, 0, 12, 30, 24, 29, 5, 24, 30, 26, 31, 22, 24, 34, 89, 31, 33, 89, 89, 32, 89, 89, 1, 84, 1866933763, 1, 90, 847799738, 12, 90, 90, 84, 16, 90, 89, 90, 11, 84, 84, 90, 1, 85, 12952, 1, 86, 62168, 39, 5078, 1, 84, 2290724431, 1, 85, 6247, 1, 86, 29577, 39, 5078, 0, 32, 60, 0, 12, 33, 24, 32, 5, 24, 33, 26, 34, 22, 24, 34, 89, 34, 33, 89, 89, 1, 84, 1606455765, 1, 90, 2646401396, 12, 90, 90, 84, 13, 90, 90, 89, 11, 84, 84, 90, 1, 85, 58723, 1, 86, 14773, 39, 5078, 1, 84, 1434615667, 1, 85, 13654, 1, 86, 507, 39, 5078, 0, 35, 61, 0, 11, 36, 24, 35, 5, 24, 36, 26, 37, 22, 24, 34, 89, 37, 33, 89, 89, 1, 84, 933927959, 1, 90, 1526696062, 12, 90, 90, 84, 13, 90, 90, 89, 11, 84, 84, 90, 1, 85, 2355, 1, 86, 52924, 39, 5078, 1, 84, 1217042035, 1, 85, 40769, 1, 86, 53939, 39, 5078, 0, 38, 62, 0, 11, 39, 24, 38, 5, 24, 39, 26, 40, 22, 24, 34, 89, 40, 33, 89, 89, 1, 84, 2657359803, 1, 90, 2085403306, 12, 90, 90, 84, 13, 90, 90, 89, 11, 84, 84, 90, 1, 85, 12020, 1, 86, 3093, 39, 5078, 1, 84, 2228963166, 1, 85, 26006, 1, 86, 7821, 39, 5078, 0, 41, 63, 0, 12, 42, 24, 41, 5, 24, 42, 26, 43, 22, 24, 34, 89, 43, 33, 89, 89, 1, 84, 1955186943, 1, 90, 1653599990, 12, 90, 90, 84, 13, 90, 90, 89, 11, 84, 84, 90, 1, 85, 53854, 1, 86, 64148, 39, 5078, 1, 84, 2916934951, 1, 85, 44927, 1, 86, 13008, 39, 5078, 0, 44, 64, 0, 12, 45, 24, 44, 5, 24, 45, 26, 46, 22, 24, 34, 89, 46, 33, 89, 89, 32, 89, 89, 1, 84, 4286637455, 1, 90, 3870677336, 12, 90, 90, 84, 16, 90, 89, 90, 11, 84, 84, 90, 1, 85, 2891, 1, 86, 34498, 39, 5078, 1, 84, 1233789041, 1, 85, 8187, 1, 86, 16791, 39, 5078, 0, 47, 65, 0, 12, 48, 24, 47, 5, 24, 48, 26, 49, 22, 24, 34, 89, 49, 33, 89, 89, 32, 89, 89, 1, 84, 3096521545, 1, 90, 3782851208, 12, 90, 90, 84, 16, 90, 89, 90, 11, 84, 84, 90, 1, 85, 21157, 1, 86, 23015, 39, 5078, 1, 84, 4151059808, 1, 85, 12933, 1, 86, 50901, 39, 5078, 0, 50, 66, 0, 11, 51, 24, 50, 5, 24, 51, 26, 52, 22, 24, 34, 89, 52, 33, 89, 89, 1, 84, 1369518636, 1, 90, 776750533, 12, 90, 90, 84, 13, 90, 90, 89, 11, 84, 84, 90, 1, 85, 11938, 1, 86, 18405, 39, 5078, 1, 84, 23325567, 1, 85, 892, 1, 86, 27180, 39, 5078, 0, 53, 67, 0, 12, 54, 24, 53, 5, 24, 54, 26, 55, 22, 24, 34, 89, 55, 33, 89, 89, 1, 84, 1978192225, 1, 90, 2123782800, 12, 90, 90, 84, 13, 90, 89, 90, 11, 84, 84, 90, 1, 85, 16191, 1, 86, 24365, 39, 5078, 1, 84, 1637186374, 1, 85, 10132, 1, 86, 15582, 39, 5078, 0, 56, 68, 0, 11, 57, 24, 56, 5, 24, 57, 26, 58, 22, 24, 34, 89, 58, 33, 89, 89, 32, 89, 89, 1, 84, 2387733456, 1, 90, 1056606675, 12, 90, 90, 84, 16, 90, 90, 89, 11, 84, 84, 90, 1, 85, 23629, 1, 86, 25126, 39, 5078, 1, 84, 450025568, 1, 85, 19347, 1, 86, 39257, 39, 5078, 0, 59, 69, 0, 12, 60, 24, 59, 5, 24, 60, 26, 61, 22, 24, 34, 89, 61, 33, 89, 89, 1, 84, 2987126258, 1, 90, 1345094725, 12, 90, 90, 84, 13, 90, 90, 89, 11, 84, 84, 90, 1, 85, 50485, 1, 86, 24243, 39, 5078, 1, 84, 1163631303, 1, 85, 34349, 1, 86, 32704, 39, 5078, 0, 62, 70, 0, 12, 63, 24, 62, 5, 24, 63, 26, 64, 22, 24, 34, 89, 64, 33, 89, 89, 32, 89, 89, 1, 84, 2369549816, 1, 90, 1506412169, 12, 90, 90, 84, 16, 90, 89, 90, 11, 84, 84, 90, 1, 85, 34247, 1, 86, 26506, 39, 5078, 1, 84, 2589904364, 1, 85, 22231, 1, 86, 26013, 39, 5078, 0, 65, 71, 0, 11, 66, 24, 65, 5, 24, 66, 26, 67, 22, 24, 34, 89, 67, 33, 89, 89, 32, 89, 89, 1, 84, 3064966724, 1, 90, 3886481159, 12, 90, 90, 84, 16, 90, 90, 89, 11, 84, 84, 90, 1, 85, 43164, 1, 86, 39191, 39, 5078, 1, 84, 461130956, 1, 85, 61189, 1, 86, 35590, 39, 5078, 0, 68, 72, 0, 12, 69, 24, 68, 5, 24, 69, 26, 70, 22, 24, 34, 89, 70, 33, 89, 89, 1, 84, 310845128, 1, 90, 2068337831, 12, 90, 90, 84, 13, 90, 89, 90, 11, 84, 84, 90, 1, 85, 13687, 1, 86, 4396, 39, 5078, 1, 84, 1143449385, 1, 85, 36045, 1, 86, 10529, 39, 5078, 0, 71, 73, 0, 11, 72, 24, 71, 5, 24, 72, 26, 73, 22, 24, 34, 89, 73, 33, 89, 89, 1, 84, 3186355405, 1, 90, 2146409774, 12, 90, 90, 84, 13, 90, 89, 90, 11, 84, 84, 90, 1, 85, 18196, 1, 86, 20155, 39, 5078, 1, 84, 3856900567, 1, 85, 22673, 1, 86, 55899, 39, 5078, 0, 74, 74, 0, 12, 75, 24, 74, 5, 24, 75, 26, 76, 22, 24, 34, 89, 76, 33, 89, 89, 32, 89, 89, 1, 84, 562002843, 1, 90, 2049126002, 12, 90, 90, 84, 16, 90, 89, 90, 11, 84, 84, 90, 1, 85, 11918, 1, 86, 22034, 39, 5078, 1, 84, 3304169675, 1, 85, 46729, 1, 86, 46060, 39, 5078, 0, 77, 75, 0, 12, 78, 24, 77, 5, 24, 78, 26, 79, 22, 24, 34, 89, 79, 33, 89, 89, 1, 84, 907172136, 1, 90, 4162193801, 12, 90, 90, 84, 13, 90, 90, 89, 11, 84, 84, 90, 1, 85, 48510, 1, 86, 30636, 39, 5078, 1, 84, 800593819, 1, 85, 22608, 1, 86, 43763, 39, 5078, 0, 80, 76, 0, 11, 81, 24, 80, 5, 24, 81, 26, 82, 22, 24, 34, 89, 82, 33, 89, 89, 32, 89, 89, 1, 84, 2148469534, 1, 90, 1399705053, 12, 90, 90, 84, 16, 90, 89, 90, 11, 84, 84, 90, 1, 85, 53944, 1, 86, 21956, 39, 5078, 1, 84, 3968529604, 1, 85, 13061, 1, 86, 9902, 39, 5078, 1, 84, 2932187666, 1, 85, 60028, 1, 86, 51081, 39, 5078, 4, 5, 0, 91, 54, 0, 5, 6, 91, 1, 83, 8613, 12, 22, 22, 83, 1, 84, 121735282, 1, 85, 50515, 1, 86, 63080, 39, 5078, 21, 91, 1, 11, 18, 12, 1, 91, 0, 13, 77, 0, 1, 83, 24810, 12, 22, 22, 83, 1, 84, 2786873358, 1, 85, 19586, 1, 86, 33755, 39, 5078, 5, 91, 7, 0, 91, 2, 0, 11, 19, 7, 91, 1, 83, 52499, 11, 22, 22, 83, 1, 84, 1033147428, 1, 85, 64489, 1, 86, 12781, 39, 5078, 0, 91, 4, 0, 45, 91, 1, 83, 9962, 12, 22, 22, 83, 1, 84, 327427119, 1, 85, 41804, 1, 86, 15651, 39, 5078, 18, 91, 17, 8, 43, 92, 14, 15, 1, 91, 11, 18, 6, 92, 1, 83, 30295, 12, 22, 22, 83, 1, 84, 1134621053, 1, 85, 19690, 1, 86, 55736, 39, 5078, 8, 91, 0, 16, 11, 92, 2, 7, 43, 17, 0, 91, 1, 92, 1, 83, 38368, 11, 22, 22, 83, 1, 84, 4056930445, 1, 85, 46094, 1, 86, 5928, 39, 5078, 1, 83, 5711, 11, 22, 22, 83, 1, 84, 714293799, 1, 85, 27773, 1, 86, 52253, 39, 5078, 0, 91, 78, 0, 11, 9, 1, 91, 0, 10, 6, 0, 1, 83, 17441, 12, 22, 22, 83, 1, 84, 845823410, 1, 85, 11312, 1, 86, 57877, 39, 5078, 0, 91, 6, 0, 5, 7, 91, 1, 83, 29066, 12, 22, 22, 83, 1, 84, 545626110, 1, 85, 50816, 1, 86, 12675, 39, 5078, 22, 91, 7, 3, 34, 89, 91, 33, 89, 89, 1, 84, 2524781636, 1, 90, 4157495370, 12, 90, 90, 84, 13, 90, 90, 89, 11, 84, 84, 90, 1, 85, 28747, 1, 86, 4250, 39, 5078, 1, 83, 34082, 11, 22, 22, 83, 1, 84, 2711728459, 1, 85, 52202, 1, 86, 6505, 39, 5078, 1, 83, 43676, 11, 22, 22, 83, 1, 84, 1880586033, 1, 85, 53204, 1, 86, 9724, 39, 5078, 45, 6, 5, 7, 19, 1, 83, 42451, 12, 22, 22, 83, 1, 84, 565685757, 1, 85, 55946, 1, 86, 58797, 39, 5078, 0, 91, 51, 0, 8, 15, 14, 91, 0, 16, 48, 0, 1, 83, 41991, 12, 22, 22, 83, 1, 84, 4201865807, 1, 85, 42141, 1, 86, 25345, 39, 5078, 1, 83, 3580, 11, 22, 22, 83, 1, 84, 1847456379, 1, 85, 46095, 1, 86, 54231, 39, 5078, 5, 6, 18, 1, 83, 23263, 12, 22, 22, 83, 1, 84, 1006524377, 1, 85, 64634, 1, 86, 29475, 39, 5078, 16, 91, 12, 13, 5, 8, 91, 2, 14, 50, 0, 1, 83, 61213, 11, 22, 22, 83, 1, 84, 852888117, 1, 85, 32039, 1, 86, 3859, 39, 5078, 17, 91, 9, 10, 5, 1, 91, 0, 11, 79, 0, 1, 83, 15505, 12, 22, 22, 83, 1, 84, 3209903590, 1, 85, 51442, 1, 86, 45278, 39, 5078, 1, 83, 44211, 22, 83, 22, 83, 34, 89, 83, 33, 89, 89, 32, 89, 89, 1, 84, 204694387, 1, 90, 1746385237, 12, 90, 90, 84, 16, 90, 89, 90, 11, 84, 84, 90, 1, 85, 46067, 1, 86, 17140, 39, 5078, 1, 83, 48388, 11, 22, 22, 83, 1, 84, 3842780280, 1, 85, 11620, 1, 86, 65475, 39, 5078, 1, 83, 22575, 11, 22, 22, 83, 1, 84, 1704027235, 1, 85, 622, 1, 86, 54015, 39, 5078, 42, 84, 87, 3, 84, 85, 86, 1, 88, 0, 8, 84, 84, 88, 58, 84, 4, 4, 0, 6, 80, 0, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 6, 0, 21, 8, 6, 7, 5, 5, 8, 0, 6, 81, 0, 12, 7, 0, 6, 0, 6, 6, 0, 21, 8, 7, 6, 5, 0, 8, 18, 6, 0, 5, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 82, 0, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 2, 6, 83, 0, 0, 7, 84, 0, 8, 8, 6, 7, 0, 7, 85, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 6, 0, 21, 7, 9, 6, 5, 0, 7, 0, 6, 86, 0, 11, 7, 0, 6, 0, 6, 6, 0, 21, 8, 7, 6, 5, 0, 8, 45, 0, 0, 6, 4, 0, 45, 6, 4, 4, 0, 6, 80, 0, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 6, 0, 21, 8, 6, 7, 5, 5, 8, 0, 6, 80, 0, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 18, 6, 0, 5, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 87, 0, 18, 7, 0, 6, 0, 6, 6, 0, 21, 8, 7, 6, 5, 0, 8, 2, 6, 83, 0, 0, 7, 84, 0, 8, 8, 6, 7, 0, 7, 88, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 6, 0, 21, 7, 9, 6, 5, 0, 7, 0, 6, 55, 0, 49, 7, 1, 6, 0, 45, 7, 0, 6, 4, 0, 45, 6, 4, 4, 0, 6, 80, 0, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 6, 0, 21, 8, 6, 7, 5, 5, 8, 2, 6, 83, 0, 0, 7, 84, 0, 8, 8, 6, 7, 0, 7, 89, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 6, 0, 21, 7, 9, 6, 5, 0, 7, 0, 6, 90, 0, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 91, 0, 18, 7, 0, 6, 0, 6, 6, 0, 21, 8, 7, 6, 5, 0, 8, 18, 6, 0, 5, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 92, 0, 12, 7, 0, 6, 0, 6, 6, 0, 21, 8, 7, 6, 5, 0, 8, 0, 6, 93, 0, 18, 7, 0, 6, 0, 6, 6, 0, 21, 8, 7, 6, 5, 0, 8, 48, 6, 1, 0, 45, 6, 0, 6, 4, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 17; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/qualification.json new file mode 100644 index 00000000..b8dffb79 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/qualification.json @@ -0,0 +1,74 @@ +{ + "id": "exceptions--stringConcealingControlFlowFlatteningDispatcher--61", + "case": "exceptions", + "profile": "stringConcealingControlFlowFlatteningDispatcher", + "seed": 61, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/encoded.js new file mode 100644 index 00000000..e302c384 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"UrV64Tu7PAtbM6WS02ttZACF98mYoJ27+mUpvfprmdmkel19X9gS8NSdnPeuM0QDFiRP9GzL3qskN1a4mt1DM1QnG9Wcd8Sd/Y3Nf/saUzf1xfqRYA4zy5dBX48o0RIJ2bjwGqPbAt7W/O1etEE1wBvn5SoD0Iy9WF2TTl67y+/obsMA8oNO9x7jLyO1wiXEPo1+cog1TcZEOVRe9z/wluvSq0iBXNVQKuDrWD26NN+BINwwbMcRKaXWsze1PFrLdwU8dYFK7ngZqJAGA3DRdNJfcwj8THQv3DirW1dXpoAo+bbqo5qQfywbfkmcGzdn/saYNc8uGEhX3u823Jjw4fzvZl/IenTjlw95xDb/lgGxZy1SfJ0yLlvnpzykhwL3zpjq89Y6au+JxxhO69MhY1HoHsh0b6Z1T+NsRpfT5m2fo3iOaW5bufeyzcQhxKqcVcFJRte6ewPj2jYRhvn+j8uoN/+bPsqtAg54gYL6hLYPuvLF2HIy37IjJKenEgCNgQXgmuYO4ru797umNSKqb4DeSqlhzoLFH1pUPCWurLyQYLQm5+brevqP1BvOhhGo1oMeBZfDXA/ijEI6OndSzMT5sE0h3wYcqReTjy0pkzjHsukdKfMLQhxTYFt/QX1IRoINoXvX+24otpoOEp1Zahrw+uuvhOCChKPaz708d4IjeLzIzpRMbmJojigw9UR65K+H1Q0ZDlgcrzee+PEsFh4xAU+uD/eRRMzHGOHaIB36vqF+7O0BKyPtBXtiDHtjLOcGRJeEAHsE9Y9790z4Jj6WzvK+AVp5f5UeTmmSP+Ejx5tKFWoWIAzfdk6ig4UWl8zti7vdYIM8mXRA6sPTA4LhNiU0a3tAlPmg4K8vOrU56DgmnilMrCwyA/+8z67QjqnH70coKBm3ACTCKiTQy1LEdPDbQiqrhn0edcJ7Y8JLF/EVMOD05hLJ2XS7DE3tEqwKP0DDsXoOPIVK7VfMfYdIbrDGXCvdjrRTlG5Ls/Oy3rsdKijGhKRblloSp8KexEXx9caTZvQS8KmuE7akEe3F3OepoIbjHUNUhUVJjiLtm48hV6P5p87LIVOIehE9rTUpzZ4fUgTr1CqPtqL+PK2B5jQpYXM5biU=", /* 1 */"window", /* 2 */1, /* 3 */3, /* 4 */undefined, /* 5 */62479, /* 6 */0, /* 7 */31733, /* 8 */61487, /* 9 */6860, /* 10 */19475, /* 11 */23651, /* 12 */49166, /* 13 */52842, /* 14 */8792, /* 15 */3108, /* 16 */29102, /* 17 */32762, /* 18 */51053, /* 19 */19354, /* 20 */6750, /* 21 */37405, /* 22 */41769, /* 23 */2, /* 24 */"z8pzlb", /* 25 */44052, /* 26 */58779, /* 27 */10109, /* 28 */29508, /* 29 */10954, /* 30 */13023, /* 31 */43706, /* 32 */112, /* 33 */14441, /* 34 */30375, /* 35 */48947, /* 36 */50640, /* 37 */56217, /* 38 */25953, /* 39 */30313, /* 40 */504, /* 41 */17142, /* 42 */33464, /* 43 */12285, /* 44 */18919, /* 45 */"charCodeAt", /* 46 */"length", /* 47 */"atob", /* 48 */"", /* 49 */8, /* 50 */"String", /* 51 */"fromCharCode", /* 52 */64323, /* 53 */26325, /* 54 */46472, /* 55 */38642, /* 56 */40106, /* 57 */15982, /* 58 */54382, /* 59 */39561, /* 60 */7121, /* 61 */7060, /* 62 */11542, /* 63 */42457, /* 64 */34395, /* 65 */9367, /* 66 */31600, /* 67 */33918, /* 68 */57948, /* 69 */4356, /* 70 */11838, /* 71 */45784, /* 72 */37309, /* 73 */65535, /* 74 */2654435769, /* 75 */13, /* 76 */"j9fy", /* 77 */16, /* 78 */"Math", /* 79 */"imul", /* 80 */2121494317, /* 81 */28, /* 82 */4, /* 83 */31, /* 84 */50855992, /* 85 */29, /* 86 */3913014607, /* 87 */2935970009, /* 88 */25, /* 89 */2833305471, /* 90 */4117668281]; +var BYTECODE = [47, 5, 1476, 1, 84, 0, 0, 0, 6, 0, 0, 42, 4, 5, 1, 6, 47, 7, 3285, 4, 92, 0, 0, 4, 1, 47, 11, 91, 1, 72, 1, 0, 1, 4, 5, 2, 11, 2, 11, 1, 0, 1, 8, 2114760142, 1, 9, 185, 1, 10, 11, 42, 3, 7, 4, 4, 8, 9, 10, 0, 12, 2, 0, 42, 13, 2, 1, 12, 0, 12, 3, 0, 42, 14, 2, 1, 12, 48, 12, 2, 13, 14, 9, 11, 3, 12, 0, 11, 4, 0, 45, 11, 47, 66, 4864, 3, 10, 0, 0, 4, 11, 0, 12, 5, 0, 5, 13, 12, 0, 14, 6, 0, 5, 15, 14, 0, 16, 7, 0, 27, 17, 13, 16, 34, 68, 17, 33, 68, 68, 1, 63, 4014745618, 1, 69, 582078578, 12, 69, 69, 63, 13, 69, 69, 68, 11, 63, 63, 69, 1, 64, 19854, 1, 65, 4171, 39, 1459, 0, 18, 8, 0, 5, 15, 18, 26, 19, 13, 15, 34, 68, 19, 33, 68, 68, 32, 68, 68, 1, 63, 1100801770, 1, 69, 1082906714, 12, 69, 69, 63, 16, 69, 68, 69, 11, 63, 63, 69, 1, 64, 26902, 1, 65, 23661, 39, 1459, 1, 63, 1646738516, 1, 64, 58364, 1, 65, 43904, 39, 1459, 0, 20, 9, 0, 12, 21, 15, 20, 5, 15, 21, 26, 22, 13, 15, 34, 68, 22, 33, 68, 68, 1, 63, 3152761949, 1, 69, 3134866893, 12, 69, 69, 63, 13, 69, 68, 69, 11, 63, 63, 69, 1, 64, 20931, 1, 65, 20700, 39, 1459, 1, 63, 3649307654, 1, 64, 13162, 1, 65, 1581, 39, 1459, 0, 23, 10, 0, 12, 24, 15, 23, 5, 15, 24, 26, 25, 13, 15, 34, 68, 25, 33, 68, 68, 1, 63, 2158626140, 1, 69, 2140731084, 12, 69, 69, 63, 13, 69, 69, 68, 11, 63, 63, 69, 1, 64, 63204, 1, 65, 64218, 39, 1459, 1, 63, 617501386, 1, 64, 40518, 1, 65, 29522, 39, 1459, 0, 26, 11, 0, 11, 27, 15, 26, 5, 15, 27, 26, 28, 13, 15, 34, 68, 28, 33, 68, 68, 1, 63, 2874208686, 1, 69, 2856313630, 12, 69, 69, 63, 13, 69, 68, 69, 11, 63, 63, 69, 1, 64, 24754, 1, 65, 16113, 39, 1459, 1, 63, 411264196, 1, 64, 30940, 1, 65, 770, 39, 1459, 0, 29, 12, 0, 12, 30, 15, 29, 5, 15, 30, 26, 31, 13, 15, 34, 68, 31, 33, 68, 68, 32, 68, 68, 1, 63, 22335534, 1, 69, 4440478, 12, 69, 69, 63, 16, 69, 69, 68, 11, 63, 63, 69, 1, 64, 30530, 1, 65, 52157, 39, 1459, 1, 63, 3093904534, 1, 64, 30682, 1, 65, 36667, 39, 1459, 0, 32, 13, 0, 11, 33, 15, 32, 5, 15, 33, 26, 34, 13, 15, 34, 68, 34, 33, 68, 68, 32, 68, 68, 1, 63, 1456818042, 1, 69, 1438922986, 12, 69, 69, 63, 16, 69, 69, 68, 11, 63, 63, 69, 1, 64, 62726, 1, 65, 22796, 39, 1459, 1, 63, 2827037237, 1, 64, 32267, 1, 65, 59978, 39, 1459, 0, 35, 14, 0, 12, 36, 15, 35, 5, 15, 36, 26, 37, 13, 15, 34, 68, 37, 33, 68, 68, 32, 68, 68, 1, 63, 505552280, 1, 69, 487657224, 12, 69, 69, 63, 16, 69, 68, 69, 11, 63, 63, 69, 1, 64, 5112, 1, 65, 29840, 39, 1459, 1, 63, 3370570578, 1, 64, 14126, 1, 65, 31726, 39, 1459, 0, 38, 15, 0, 12, 39, 15, 38, 5, 15, 39, 26, 40, 13, 15, 34, 68, 40, 33, 68, 68, 32, 68, 68, 1, 63, 1006290376, 1, 69, 988395320, 12, 69, 69, 63, 16, 69, 68, 69, 11, 63, 63, 69, 1, 64, 24792, 1, 65, 14727, 39, 1459, 1, 63, 802694630, 1, 64, 15674, 1, 65, 45836, 39, 1459, 0, 41, 16, 0, 12, 42, 15, 41, 5, 15, 42, 26, 43, 13, 15, 34, 68, 43, 33, 68, 68, 32, 68, 68, 1, 63, 2770041367, 1, 69, 2752146311, 12, 69, 69, 63, 16, 69, 68, 69, 11, 63, 63, 69, 1, 64, 41369, 1, 65, 45878, 39, 1459, 1, 63, 1607516810, 1, 64, 23414, 1, 65, 54060, 39, 1459, 0, 44, 17, 0, 11, 45, 15, 44, 5, 15, 45, 26, 46, 13, 15, 34, 68, 46, 33, 68, 68, 32, 68, 68, 1, 63, 864730911, 1, 69, 846835855, 12, 69, 69, 63, 16, 69, 68, 69, 11, 63, 63, 69, 1, 64, 20897, 1, 65, 1943, 39, 1459, 1, 63, 3438977281, 1, 64, 65535, 1, 65, 65038, 39, 1459, 0, 47, 18, 0, 12, 48, 15, 47, 5, 15, 48, 26, 49, 13, 15, 34, 68, 49, 33, 68, 68, 32, 68, 68, 1, 63, 3332110921, 1, 69, 3314215865, 12, 69, 69, 63, 16, 69, 68, 69, 11, 63, 63, 69, 1, 64, 2951, 1, 65, 22358, 39, 1459, 1, 63, 3018423776, 1, 64, 6496, 1, 65, 8204, 39, 1459, 0, 50, 19, 0, 11, 51, 15, 50, 5, 15, 51, 26, 52, 13, 15, 34, 68, 52, 33, 68, 68, 32, 68, 68, 1, 63, 1462281843, 1, 69, 1444386787, 12, 69, 69, 63, 16, 69, 68, 69, 11, 63, 63, 69, 1, 64, 21613, 1, 65, 43417, 39, 1459, 1, 63, 3123475301, 1, 64, 65083, 1, 65, 47163, 39, 1459, 0, 53, 20, 0, 12, 54, 15, 53, 5, 15, 54, 26, 55, 13, 15, 34, 68, 55, 33, 68, 68, 1, 63, 785859479, 1, 69, 767964423, 12, 69, 69, 63, 13, 69, 69, 68, 11, 63, 63, 69, 1, 64, 3433, 1, 65, 16552, 39, 1459, 1, 63, 1173788984, 1, 64, 30872, 1, 65, 14381, 39, 1459, 0, 56, 21, 0, 11, 57, 15, 56, 5, 15, 57, 26, 58, 13, 15, 34, 68, 58, 33, 68, 68, 32, 68, 68, 1, 63, 4212354290, 1, 69, 4194459234, 12, 69, 69, 63, 16, 69, 69, 68, 11, 63, 63, 69, 1, 64, 55582, 1, 65, 22331, 39, 1459, 1, 63, 753408024, 1, 64, 14536, 1, 65, 24445, 39, 1459, 0, 59, 22, 0, 12, 60, 15, 59, 5, 15, 60, 26, 61, 13, 15, 34, 68, 61, 33, 68, 68, 32, 68, 68, 1, 63, 3994401604, 1, 69, 3976506548, 12, 69, 69, 63, 16, 69, 68, 69, 11, 63, 63, 69, 1, 64, 32732, 1, 65, 18185, 39, 1459, 1, 63, 687701550, 1, 64, 55810, 1, 65, 40118, 39, 1459, 1, 63, 1322465676, 1, 64, 40852, 1, 65, 64118, 39, 1459, 1, 8, 1135751136, 1, 9, 197, 1, 10, 3, 1, 62, 13675, 11, 13, 13, 62, 1, 63, 3110842463, 1, 64, 10433, 1, 65, 36830, 39, 1459, 0, 70, 2, 0, 26, 71, 0, 70, 34, 68, 71, 33, 68, 68, 32, 68, 68, 1, 63, 2163610300, 1, 69, 3364247836, 12, 69, 69, 63, 16, 69, 68, 69, 11, 63, 63, 69, 1, 64, 38596, 1, 65, 43390, 39, 1459, 1, 62, 12903, 11, 13, 13, 62, 1, 63, 1073621953, 1, 64, 44383, 1, 65, 2379, 39, 1459, 1, 62, 6451, 12, 13, 13, 62, 1, 63, 351608713, 1, 64, 47511, 1, 65, 13396, 39, 1459, 0, 70, 4, 0, 45, 70, 1, 8, 1028113336, 1, 9, 210, 1, 10, 5, 1, 62, 7248, 11, 13, 13, 62, 1, 63, 823648194, 1, 64, 63838, 1, 65, 6193, 39, 1459, 1, 62, 940, 12, 13, 13, 62, 1, 63, 1462723356, 1, 64, 30212, 1, 65, 61978, 39, 1459, 42, 3, 7, 4, 6, 8, 9, 10, 45, 3, 1, 62, 11114, 11, 13, 13, 62, 1, 63, 509534553, 1, 64, 63431, 1, 65, 10986, 39, 1459, 1, 8, 995264652, 1, 9, 202, 1, 10, 3, 1, 62, 37897, 11, 13, 13, 62, 1, 63, 1980675847, 1, 64, 8729, 1, 65, 54075, 39, 1459, 3, 6, 0, 47, 7, 3285, 4, 92, 0, 0, 4, 2, 1, 62, 52842, 12, 13, 13, 62, 1, 63, 253213726, 1, 64, 7426, 1, 65, 14898, 39, 1459, 1, 62, 4564, 12, 13, 13, 62, 1, 63, 3096262518, 1, 64, 41386, 1, 65, 37052, 39, 1459, 1, 62, 18291, 11, 13, 13, 62, 1, 63, 2076046682, 1, 64, 58310, 1, 65, 52619, 39, 1459, 1, 62, 37405, 12, 13, 13, 62, 1, 63, 756225533, 1, 64, 49955, 1, 65, 7222, 39, 1459, 42, 4, 7, 4, 6, 8, 9, 10, 45, 4, 0, 70, 23, 0, 26, 71, 0, 70, 34, 68, 71, 33, 68, 68, 32, 68, 68, 1, 63, 3662702878, 1, 69, 568373118, 12, 69, 69, 63, 16, 69, 68, 69, 11, 63, 63, 69, 1, 64, 64914, 1, 65, 31463, 39, 1459, 1, 62, 36263, 11, 13, 13, 62, 1, 63, 2133383619, 1, 64, 65373, 1, 65, 51744, 39, 1459, 1, 62, 30655, 11, 13, 13, 62, 1, 63, 2820127247, 1, 64, 7953, 1, 65, 41266, 39, 1459, 42, 5, 7, 4, 6, 8, 9, 10, 45, 5, 42, 63, 66, 3, 63, 64, 65, 0, 67, 24, 0, 8, 63, 63, 67, 58, 63, 47, 79, 5028, 3, 10, 0, 0, 4, 15, 0, 16, 25, 0, 5, 17, 16, 0, 18, 6, 0, 5, 19, 18, 0, 20, 26, 0, 27, 21, 17, 20, 34, 80, 21, 33, 80, 80, 32, 80, 80, 1, 76, 3935127738, 1, 81, 2772677042, 12, 81, 81, 76, 16, 81, 80, 81, 11, 76, 76, 81, 1, 77, 38567, 1, 78, 30276, 39, 3276, 0, 22, 27, 0, 5, 19, 22, 26, 23, 17, 19, 34, 80, 23, 33, 80, 80, 32, 80, 80, 1, 76, 551795376, 1, 81, 3968157401, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 56654, 1, 78, 13337, 39, 3276, 1, 76, 1765899233, 1, 77, 42564, 1, 78, 7121, 39, 3276, 0, 24, 28, 0, 11, 25, 19, 24, 5, 19, 25, 26, 26, 17, 19, 34, 80, 26, 33, 80, 80, 32, 80, 80, 1, 76, 1942024252, 1, 81, 1063418942, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 6823, 1, 78, 17777, 39, 3276, 1, 76, 3970856271, 1, 77, 15917, 1, 78, 52567, 39, 3276, 0, 27, 29, 0, 11, 28, 19, 27, 5, 19, 28, 26, 29, 17, 19, 34, 80, 29, 33, 80, 80, 1, 76, 3298764161, 1, 81, 2420158843, 12, 81, 81, 76, 13, 81, 80, 81, 11, 76, 76, 81, 1, 77, 16830, 1, 78, 63697, 39, 3276, 1, 76, 3641950573, 1, 77, 38409, 1, 78, 61656, 39, 3276, 0, 30, 30, 0, 11, 31, 19, 30, 5, 19, 31, 26, 32, 17, 19, 34, 80, 32, 33, 80, 80, 32, 80, 80, 1, 76, 1433389969, 1, 81, 554784739, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 4353, 1, 78, 41578, 39, 3276, 1, 76, 4042132457, 1, 77, 14801, 1, 78, 23250, 39, 3276, 0, 33, 31, 0, 12, 34, 19, 33, 5, 19, 34, 26, 35, 17, 19, 34, 80, 35, 33, 80, 80, 1, 76, 1984100441, 1, 81, 1105495131, 12, 81, 81, 76, 13, 81, 81, 80, 11, 76, 76, 81, 1, 77, 39226, 1, 78, 64459, 39, 3276, 1, 76, 45972487, 1, 77, 38257, 1, 78, 35151, 39, 3276, 0, 36, 32, 0, 11, 37, 19, 36, 5, 19, 37, 26, 38, 17, 19, 34, 80, 38, 33, 80, 80, 32, 80, 80, 1, 76, 3705938461, 1, 81, 2827333215, 12, 81, 81, 76, 16, 81, 80, 81, 11, 76, 76, 81, 1, 77, 22526, 1, 78, 2730, 39, 3276, 1, 76, 531613636, 1, 77, 22482, 1, 78, 63759, 39, 3276, 0, 39, 33, 0, 11, 40, 19, 39, 5, 19, 40, 26, 41, 17, 19, 34, 80, 41, 33, 80, 80, 1, 76, 3115889059, 1, 81, 2237283933, 12, 81, 81, 76, 13, 81, 80, 81, 11, 76, 76, 81, 1, 77, 46036, 1, 78, 44171, 39, 3276, 1, 76, 1572487274, 1, 77, 21271, 1, 78, 29324, 39, 3276, 0, 42, 34, 0, 12, 43, 19, 42, 5, 19, 43, 26, 44, 17, 19, 34, 80, 44, 33, 80, 80, 32, 80, 80, 1, 76, 4148982084, 1, 81, 3270376790, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 8180, 1, 78, 8332, 39, 3276, 1, 76, 1971977844, 1, 77, 19865, 1, 78, 51871, 39, 3276, 0, 45, 35, 0, 11, 46, 19, 45, 5, 19, 46, 26, 47, 17, 19, 34, 80, 47, 33, 80, 80, 32, 80, 80, 1, 76, 3286380793, 1, 81, 2407775603, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 19447, 1, 78, 8876, 39, 3276, 1, 76, 3814291969, 1, 77, 27101, 1, 78, 12355, 39, 3276, 0, 48, 36, 0, 12, 49, 19, 48, 5, 19, 49, 26, 50, 17, 19, 34, 80, 50, 33, 80, 80, 32, 80, 80, 1, 76, 1520241319, 1, 81, 641636049, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 54475, 1, 78, 2951, 39, 3276, 1, 76, 1618265311, 1, 77, 41169, 1, 78, 16474, 39, 3276, 0, 51, 37, 0, 11, 52, 19, 51, 5, 19, 52, 26, 53, 17, 19, 34, 80, 53, 33, 80, 80, 32, 80, 80, 1, 76, 3982340750, 1, 81, 3103735560, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 41928, 1, 78, 46306, 39, 3276, 1, 76, 4041503296, 1, 77, 52576, 1, 78, 11167, 39, 3276, 0, 54, 38, 0, 12, 55, 19, 54, 5, 19, 55, 26, 56, 17, 19, 34, 80, 56, 33, 80, 80, 32, 80, 80, 1, 76, 4185516993, 1, 81, 3306911667, 12, 81, 81, 76, 16, 81, 80, 81, 11, 76, 76, 81, 1, 77, 174, 1, 78, 61202, 39, 3276, 1, 76, 3129259313, 1, 77, 65494, 1, 78, 29505, 39, 3276, 0, 57, 39, 0, 11, 58, 19, 57, 5, 19, 58, 26, 59, 17, 19, 34, 80, 59, 33, 80, 80, 32, 80, 80, 1, 76, 4080397465, 1, 81, 3201792219, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 25758, 1, 78, 35725, 39, 3276, 1, 76, 894625768, 1, 77, 31420, 1, 78, 37860, 39, 3276, 0, 60, 40, 0, 11, 61, 19, 60, 5, 19, 61, 26, 62, 17, 19, 34, 80, 62, 33, 80, 80, 1, 76, 1911350343, 1, 81, 1032745073, 12, 81, 81, 76, 13, 81, 80, 81, 11, 76, 76, 81, 1, 77, 29552, 1, 78, 14642, 39, 3276, 1, 76, 1772082313, 1, 77, 17058, 1, 78, 52287, 39, 3276, 0, 63, 41, 0, 12, 64, 19, 63, 5, 19, 64, 26, 65, 17, 19, 34, 80, 65, 33, 80, 80, 32, 80, 80, 1, 76, 4017756648, 1, 81, 3139151402, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 45249, 1, 78, 42291, 39, 3276, 1, 76, 338575433, 1, 77, 8340, 1, 78, 14719, 39, 3276, 0, 66, 42, 0, 12, 67, 19, 66, 5, 19, 67, 26, 68, 17, 19, 34, 80, 68, 33, 80, 80, 32, 80, 80, 1, 76, 2717483837, 1, 81, 1838878519, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 42911, 1, 78, 43670, 39, 3276, 1, 76, 263363019, 1, 77, 52687, 1, 78, 23823, 39, 3276, 0, 69, 43, 0, 11, 70, 19, 69, 5, 19, 70, 26, 71, 17, 19, 34, 80, 71, 33, 80, 80, 32, 80, 80, 1, 76, 3054626038, 1, 81, 2176020648, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 32631, 1, 78, 9158, 39, 3276, 1, 76, 3972446116, 1, 77, 27481, 1, 78, 2546, 39, 3276, 0, 72, 44, 0, 11, 73, 19, 72, 5, 19, 73, 26, 74, 17, 19, 34, 80, 74, 33, 80, 80, 1, 76, 4250299488, 1, 81, 3371694258, 12, 81, 81, 76, 13, 81, 80, 81, 11, 76, 76, 81, 1, 77, 56388, 1, 78, 14425, 39, 3276, 1, 76, 2756395401, 1, 77, 58852, 1, 78, 34399, 39, 3276, 1, 76, 3300410160, 1, 77, 6334, 1, 78, 38038, 39, 3276, 45, 4, 1, 75, 0, 11, 17, 17, 75, 1, 76, 476353338, 1, 77, 36534, 1, 78, 27040, 39, 3276, 19, 82, 12, 14, 17, 83, 10, 82, 43, 9, 7, 8, 1, 83, 1, 75, 50606, 11, 17, 17, 75, 1, 76, 3001109877, 1, 77, 63360, 1, 78, 63184, 39, 3276, 0, 82, 45, 0, 8, 83, 3, 82, 43, 10, 3, 83, 1, 5, 1, 75, 16130, 12, 17, 17, 75, 1, 76, 1990769367, 1, 77, 2504, 1, 78, 48728, 39, 3276, 0, 82, 46, 0, 8, 83, 3, 82, 22, 82, 5, 83, 34, 80, 82, 33, 80, 80, 32, 80, 80, 1, 76, 1294998501, 1, 81, 2732716113, 12, 81, 81, 76, 16, 81, 81, 80, 11, 76, 76, 81, 1, 77, 31108, 1, 78, 34971, 39, 3276, 1, 75, 23977, 12, 17, 17, 75, 1, 76, 214233691, 1, 77, 33496, 1, 78, 19348, 39, 3276, 1, 75, 61221, 12, 17, 17, 75, 1, 76, 2336402655, 1, 77, 52103, 1, 78, 48068, 39, 3276, 1, 75, 38783, 22, 75, 17, 75, 34, 80, 75, 33, 80, 80, 1, 76, 2223940568, 1, 81, 3661658292, 12, 81, 81, 76, 13, 81, 81, 80, 11, 76, 76, 81, 1, 77, 46138, 1, 78, 13581, 39, 3276, 1, 75, 33457, 11, 17, 17, 75, 1, 76, 557570557, 1, 77, 42456, 1, 78, 2309, 39, 3276, 1, 75, 21067, 12, 17, 17, 75, 1, 76, 3868080296, 1, 77, 23054, 1, 78, 36955, 39, 3276, 4, 2, 2, 82, 47, 0, 42, 6, 82, 1, 0, 1, 75, 8961, 11, 17, 17, 75, 1, 76, 761832007, 1, 77, 3293, 1, 78, 51072, 39, 3276, 1, 75, 17190, 22, 75, 17, 75, 34, 80, 75, 33, 80, 80, 1, 76, 3023095267, 1, 81, 165845640, 12, 81, 81, 76, 13, 81, 81, 80, 11, 76, 76, 81, 1, 77, 29355, 1, 78, 37173, 39, 3276, 1, 75, 17515, 12, 17, 17, 75, 1, 76, 1955890105, 1, 77, 37031, 1, 78, 32410, 39, 3276, 1, 75, 38702, 11, 17, 17, 75, 1, 76, 2355886954, 1, 77, 41403, 1, 78, 23455, 39, 3276, 0, 82, 4, 0, 45, 82, 11, 82, 4, 9, 5, 4, 82, 1, 75, 30817, 12, 17, 17, 75, 1, 76, 2256963211, 1, 77, 20553, 1, 78, 15452, 39, 3276, 0, 82, 23, 0, 11, 83, 5, 82, 5, 5, 83, 1, 75, 30957, 11, 17, 17, 75, 1, 76, 1017663184, 1, 77, 5961, 1, 78, 42392, 39, 3276, 1, 75, 4690, 22, 75, 17, 75, 34, 80, 75, 33, 80, 80, 1, 76, 96185763, 1, 81, 1533903487, 12, 81, 81, 76, 13, 81, 80, 81, 11, 76, 76, 81, 1, 77, 24077, 1, 78, 38573, 39, 3276, 1, 75, 48947, 11, 17, 17, 75, 1, 76, 2672023488, 1, 77, 42536, 1, 78, 39236, 39, 3276, 1, 75, 0, 11, 17, 17, 75, 1, 76, 3973325660, 1, 77, 40124, 1, 78, 3986, 39, 3276, 5, 3, 6, 0, 82, 48, 0, 5, 4, 82, 1, 75, 9937, 11, 17, 17, 75, 1, 76, 772166550, 1, 77, 22181, 1, 78, 59244, 39, 3276, 11, 82, 5, 13, 43, 12, 3, 11, 1, 82, 0, 14, 49, 0, 1, 75, 33464, 12, 17, 17, 75, 1, 76, 700965838, 1, 77, 9388, 1, 78, 59380, 39, 3276, 1, 75, 29508, 11, 17, 17, 75, 1, 76, 840079167, 1, 77, 52796, 1, 78, 50922, 39, 3276, 0, 82, 45, 0, 8, 11, 3, 82, 0, 13, 2, 0, 1, 75, 11871, 11, 17, 17, 75, 1, 76, 727710560, 1, 77, 8631, 1, 78, 42945, 39, 3276, 2, 7, 50, 0, 0, 82, 51, 0, 8, 8, 7, 82, 1, 75, 48198, 11, 17, 17, 75, 1, 76, 3329018487, 1, 77, 34772, 1, 78, 46175, 39, 3276, 0, 82, 6, 0, 5, 5, 82, 1, 75, 644, 11, 17, 17, 75, 1, 76, 4139591398, 1, 77, 31825, 1, 78, 52821, 39, 3276, 42, 76, 79, 3, 76, 77, 78, 58, 76, 47, 86, 5183, 2, 8, 0, 0, 4, 20, 0, 21, 52, 0, 5, 22, 21, 0, 23, 6, 0, 5, 24, 23, 0, 25, 53, 0, 27, 26, 22, 25, 34, 88, 26, 33, 88, 88, 32, 88, 88, 1, 84, 2818092569, 1, 89, 2515464237, 12, 89, 89, 84, 16, 89, 88, 89, 11, 84, 84, 89, 1, 85, 566800617, 39, 4848, 0, 27, 54, 0, 5, 24, 27, 26, 28, 22, 24, 34, 88, 28, 33, 88, 88, 1, 84, 1438035730, 1, 89, 2101302206, 12, 89, 89, 84, 13, 89, 89, 88, 11, 84, 84, 89, 1, 85, 968222051, 39, 4848, 1, 84, 2766183680, 1, 85, 33240406, 39, 4848, 0, 29, 55, 0, 12, 30, 24, 29, 5, 24, 30, 26, 31, 22, 24, 34, 88, 31, 33, 88, 88, 1, 84, 1101575964, 1, 89, 1764842416, 12, 89, 89, 84, 13, 89, 89, 88, 11, 84, 84, 89, 1, 85, 3468429619, 39, 4848, 1, 84, 1177872408, 1, 85, 3534987073, 39, 4848, 0, 32, 56, 0, 11, 33, 24, 32, 5, 24, 33, 26, 34, 22, 24, 34, 88, 34, 33, 88, 88, 1, 84, 2236071181, 1, 89, 2899337665, 12, 89, 89, 84, 13, 89, 89, 88, 11, 84, 84, 89, 1, 85, 202714868, 39, 4848, 1, 84, 362098560, 1, 85, 512583062, 39, 4848, 0, 35, 57, 0, 11, 36, 24, 35, 5, 24, 36, 26, 37, 22, 24, 34, 88, 37, 33, 88, 88, 1, 84, 398476722, 1, 89, 1061743134, 12, 89, 89, 84, 13, 89, 89, 88, 11, 84, 84, 89, 1, 85, 4204057182, 39, 4848, 1, 84, 1351503990, 1, 85, 852537215, 39, 4848, 0, 38, 58, 0, 12, 39, 24, 38, 5, 24, 39, 26, 40, 22, 24, 34, 88, 40, 33, 88, 88, 32, 88, 88, 1, 84, 3143530086, 1, 89, 3806796530, 12, 89, 89, 84, 16, 89, 88, 89, 11, 84, 84, 89, 1, 85, 2260863819, 39, 4848, 1, 84, 2530562524, 1, 85, 1100423163, 39, 4848, 0, 41, 59, 0, 11, 42, 24, 41, 5, 24, 42, 26, 43, 22, 24, 34, 88, 43, 33, 88, 88, 32, 88, 88, 1, 84, 402954332, 1, 89, 1066220679, 12, 89, 89, 84, 16, 89, 88, 89, 11, 84, 84, 89, 1, 85, 1508332197, 39, 4848, 1, 84, 1453362384, 1, 85, 3335860869, 39, 4848, 0, 44, 60, 0, 11, 45, 24, 44, 5, 24, 45, 26, 46, 22, 24, 34, 88, 46, 33, 88, 88, 1, 84, 2868832370, 1, 89, 3532098566, 12, 89, 89, 84, 13, 89, 89, 88, 11, 84, 84, 89, 1, 85, 1206202018, 39, 4848, 1, 84, 1300995570, 1, 85, 1781269372, 39, 4848, 0, 47, 61, 0, 12, 48, 24, 47, 5, 24, 48, 26, 49, 22, 24, 34, 88, 49, 33, 88, 88, 1, 84, 347013972, 1, 89, 1010280376, 12, 89, 89, 84, 13, 89, 88, 89, 11, 84, 84, 89, 1, 85, 1596800831, 39, 4848, 1, 84, 2479680279, 1, 85, 1021192084, 39, 4848, 0, 50, 62, 0, 11, 51, 24, 50, 5, 24, 51, 26, 52, 22, 24, 34, 88, 52, 33, 88, 88, 32, 88, 88, 1, 84, 1098993567, 1, 89, 1762260019, 12, 89, 89, 84, 16, 89, 89, 88, 11, 84, 84, 89, 1, 85, 1646681165, 39, 4848, 1, 84, 1411770491, 1, 85, 2572766099, 39, 4848, 0, 53, 63, 0, 12, 54, 24, 53, 5, 24, 54, 26, 55, 22, 24, 34, 88, 55, 33, 88, 88, 1, 84, 3320582857, 1, 89, 3983849093, 12, 89, 89, 84, 13, 89, 89, 88, 11, 84, 84, 89, 1, 85, 1588839733, 39, 4848, 1, 84, 2469124195, 1, 85, 2143323693, 39, 4848, 0, 56, 64, 0, 11, 57, 24, 56, 5, 24, 57, 26, 58, 22, 24, 34, 88, 58, 33, 88, 88, 32, 88, 88, 1, 84, 3974151166, 1, 89, 342450138, 12, 89, 89, 84, 16, 89, 88, 89, 11, 84, 84, 89, 1, 85, 1737131463, 39, 4848, 1, 84, 2206189431, 1, 85, 1704810199, 39, 4848, 0, 59, 65, 0, 11, 60, 24, 59, 5, 24, 60, 26, 61, 22, 24, 34, 88, 61, 33, 88, 88, 32, 88, 88, 1, 84, 3944677975, 1, 89, 312976995, 12, 89, 89, 84, 16, 89, 89, 88, 11, 84, 84, 89, 1, 85, 2568464540, 39, 4848, 1, 84, 2418553905, 1, 85, 2332487429, 39, 4848, 0, 62, 66, 0, 12, 63, 24, 62, 5, 24, 63, 26, 64, 22, 24, 34, 88, 64, 33, 88, 88, 1, 84, 4093813089, 1, 89, 462112110, 12, 89, 89, 84, 13, 89, 88, 89, 11, 84, 84, 89, 1, 85, 288109943, 39, 4848, 1, 84, 2979102879, 1, 85, 690064589, 39, 4848, 0, 65, 67, 0, 11, 66, 24, 65, 5, 24, 66, 26, 67, 22, 24, 34, 88, 67, 33, 88, 88, 1, 84, 929807177, 1, 89, 1593073589, 12, 89, 89, 84, 13, 89, 88, 89, 11, 84, 84, 89, 1, 85, 1320896276, 39, 4848, 1, 84, 845789805, 1, 85, 3663419537, 39, 4848, 0, 68, 68, 0, 12, 69, 24, 68, 5, 24, 69, 26, 70, 22, 24, 34, 88, 70, 33, 88, 88, 32, 88, 88, 1, 84, 1608531304, 1, 89, 2271797708, 12, 89, 89, 84, 16, 89, 88, 89, 11, 84, 84, 89, 1, 85, 1444032142, 39, 4848, 1, 84, 1110148861, 1, 85, 3018634889, 39, 4848, 0, 71, 69, 0, 12, 72, 24, 71, 5, 24, 72, 26, 73, 22, 24, 34, 88, 73, 33, 88, 88, 1, 84, 3208501876, 1, 89, 3871768064, 12, 89, 89, 84, 13, 89, 89, 88, 11, 84, 84, 89, 1, 85, 2007809406, 39, 4848, 1, 84, 3524868711, 1, 85, 2868074576, 39, 4848, 0, 74, 70, 0, 11, 75, 24, 74, 5, 24, 75, 26, 76, 22, 24, 34, 88, 76, 33, 88, 88, 32, 88, 88, 1, 84, 284241512, 1, 89, 947507892, 12, 89, 89, 84, 16, 89, 88, 89, 11, 84, 84, 89, 1, 85, 1438962360, 39, 4848, 1, 84, 369978263, 1, 85, 648950533, 39, 4848, 0, 77, 71, 0, 11, 78, 24, 77, 5, 24, 78, 26, 79, 22, 24, 34, 88, 79, 33, 88, 88, 1, 84, 539247200, 1, 89, 1202513563, 12, 89, 89, 84, 13, 89, 89, 88, 11, 84, 84, 89, 1, 85, 3347704444, 39, 4848, 1, 84, 564501141, 1, 85, 837680105, 39, 4848, 0, 80, 72, 0, 12, 81, 24, 80, 5, 24, 81, 26, 82, 22, 24, 34, 88, 82, 33, 88, 88, 1, 84, 729986401, 1, 89, 1393252757, 12, 89, 89, 84, 13, 89, 88, 89, 11, 84, 84, 89, 1, 85, 1025745740, 39, 4848, 1, 84, 1786177255, 1, 85, 3424480381, 39, 4848, 1, 84, 570490250, 1, 85, 3793038384, 39, 4848, 5, 7, 19, 1, 83, 33886, 12, 22, 22, 83, 1, 84, 3532808994, 1, 85, 830719616, 39, 4848, 21, 90, 1, 11, 18, 12, 1, 90, 0, 13, 73, 0, 1, 83, 42783, 12, 22, 22, 83, 1, 84, 4084972025, 1, 85, 278556747, 39, 4848, 16, 90, 12, 13, 5, 8, 90, 2, 14, 50, 0, 1, 83, 1455, 11, 22, 22, 83, 1, 84, 348325044, 1, 85, 4015203597, 39, 4848, 22, 90, 7, 3, 34, 88, 90, 33, 88, 88, 32, 88, 88, 1, 84, 1081814899, 1, 89, 3596286759, 12, 89, 89, 84, 16, 89, 88, 89, 11, 84, 84, 89, 1, 85, 2451585863, 39, 4848, 1, 83, 37309, 11, 22, 22, 83, 1, 84, 510152526, 1, 85, 3853376138, 39, 4848, 1, 83, 38368, 11, 22, 22, 83, 1, 84, 2702476670, 1, 85, 1661052061, 39, 4848, 1, 83, 38781, 11, 22, 22, 83, 1, 84, 809399716, 1, 85, 3554128911, 39, 4848, 0, 90, 6, 0, 5, 7, 90, 1, 83, 24140, 12, 22, 22, 83, 1, 84, 2431790361, 1, 85, 1931738234, 39, 4848, 45, 6, 8, 90, 0, 16, 11, 91, 2, 7, 43, 17, 0, 90, 1, 91, 1, 83, 3541, 11, 22, 22, 83, 1, 84, 4110593246, 1, 85, 252935463, 39, 4848, 4, 5, 0, 90, 48, 0, 5, 6, 90, 1, 83, 17851, 12, 22, 22, 83, 1, 84, 1396138224, 1, 85, 2967390450, 39, 4848, 1, 83, 405, 11, 22, 22, 83, 1, 84, 3240195512, 1, 85, 1123333107, 39, 4848, 0, 90, 74, 0, 11, 9, 1, 90, 0, 10, 6, 0, 1, 83, 51164, 12, 22, 22, 83, 1, 84, 3851071926, 1, 85, 512456711, 39, 4848, 1, 83, 0, 11, 22, 22, 83, 1, 84, 2369719430, 1, 85, 1993809163, 39, 4848, 0, 90, 4, 0, 45, 90, 5, 90, 7, 0, 90, 2, 0, 11, 19, 7, 90, 1, 83, 25813, 11, 22, 22, 83, 1, 84, 72547429, 1, 85, 4290981220, 39, 4848, 5, 6, 18, 1, 83, 31600, 12, 22, 22, 83, 1, 84, 823600965, 1, 85, 3539927662, 39, 4848, 1, 83, 45543, 22, 83, 22, 83, 34, 88, 83, 33, 88, 88, 32, 88, 88, 1, 84, 2671970002, 1, 89, 891474572, 12, 89, 89, 84, 16, 89, 88, 89, 11, 84, 84, 89, 1, 85, 3753501545, 39, 4848, 1, 83, 45917, 12, 22, 22, 83, 1, 84, 1517921320, 1, 85, 2845607366, 39, 4848, 1, 83, 25604, 12, 22, 22, 83, 1, 84, 2438105941, 1, 85, 1925422766, 39, 4848, 18, 90, 17, 8, 43, 91, 14, 15, 1, 90, 11, 18, 6, 91, 1, 83, 9367, 11, 22, 22, 83, 1, 84, 3254069758, 1, 85, 1109458989, 39, 4848, 17, 90, 9, 10, 5, 1, 90, 0, 11, 75, 0, 1, 83, 39622, 11, 22, 22, 83, 1, 84, 3377127600, 1, 85, 986401015, 39, 4848, 0, 90, 51, 0, 8, 15, 14, 90, 0, 16, 45, 0, 1, 83, 41267, 11, 22, 22, 83, 1, 84, 4135862405, 1, 85, 227666125, 39, 4848, 42, 84, 86, 2, 84, 85, 0, 87, 76, 0, 8, 84, 84, 87, 58, 84, 4, 4, 0, 6, 77, 0, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 6, 0, 21, 8, 6, 7, 5, 5, 8, 11, 6, 0, 5, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 2, 6, 78, 0, 0, 7, 79, 0, 8, 8, 6, 7, 0, 7, 80, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 6, 0, 21, 7, 9, 6, 5, 0, 7, 0, 6, 81, 0, 19, 7, 0, 6, 0, 6, 82, 0, 21, 8, 0, 6, 17, 6, 7, 8, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 83, 0, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 84, 0, 12, 7, 0, 6, 0, 6, 6, 0, 21, 8, 7, 6, 5, 0, 8, 0, 6, 24, 0, 49, 7, 1, 6, 0, 45, 7, 0, 6, 4, 0, 45, 6, 4, 4, 0, 6, 77, 0, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 6, 0, 21, 8, 6, 7, 5, 5, 8, 0, 6, 85, 0, 19, 7, 0, 6, 0, 6, 3, 0, 21, 8, 0, 6, 17, 6, 7, 8, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 0, 6, 86, 0, 12, 7, 0, 6, 0, 6, 6, 0, 21, 8, 7, 6, 5, 0, 8, 0, 6, 81, 0, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 11, 6, 0, 5, 0, 7, 6, 0, 21, 8, 6, 7, 5, 0, 8, 2, 6, 78, 0, 0, 7, 79, 0, 8, 8, 6, 7, 0, 7, 87, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 6, 0, 21, 7, 9, 6, 5, 0, 7, 45, 0, 0, 6, 4, 0, 45, 6, 4, 3, 0, 4, 88, 0, 21, 5, 0, 4, 18, 4, 0, 5, 0, 5, 6, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 89, 0, 12, 5, 0, 4, 0, 4, 6, 0, 21, 6, 5, 4, 5, 0, 6, 11, 4, 0, 1, 0, 5, 6, 0, 21, 6, 4, 5, 5, 0, 6, 2, 4, 78, 0, 0, 5, 79, 0, 8, 6, 4, 5, 0, 5, 90, 0, 43, 7, 4, 6, 2, 0, 5, 0, 4, 6, 0, 21, 5, 7, 4, 5, 0, 5, 0, 4, 76, 0, 49, 5, 1, 4, 0, 45, 5, 0, 4, 4, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 15; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/qualification.json new file mode 100644 index 00000000..e067eefe --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/qualification.json @@ -0,0 +1,71 @@ +{ + "id": "switch--stringConcealingControlFlowFlatteningDispatcher--61", + "case": "switch", + "profile": "stringConcealingControlFlowFlatteningDispatcher", + "seed": 61, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/source.js new file mode 100644 index 00000000..98a67db9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-dispatcher-switch/source.js @@ -0,0 +1 @@ +function classify(value) { switch (value) { case 1: return "one"; case 2: return "two"; default: return "other"; } } window.TEST_OUTPUT = [classify(1), classify(3)]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/encoded.js new file mode 100644 index 00000000..e31de150 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */28697, /* 1 */0, /* 2 */1871, /* 3 */2030, /* 4 */15919, /* 5 */9232, /* 6 */1976, /* 7 */1201, /* 8 */12115, /* 9 */48237, /* 10 */60117, /* 11 */30778, /* 12 */25543, /* 13 */29689, /* 14 */4723, /* 15 */30065, /* 16 */24768, /* 17 */14635, /* 18 */31854, /* 19 */10409, /* 20 */20006, /* 21 */43443, /* 22 */13442, /* 23 */30628, /* 24 */5844, /* 25 */18748, /* 26 */118, /* 27 */1, /* 28 */"M1MJSbeJEuB+oct/dFsWyDEdLsmYM5pswtXRlBkkF6zfXbv4CQpK6usPxiAiMdEBqvC7c3iiCP2YO8x3Jll5DW6FaRjNDtX1Fs4pAGn7kBLt+AOH2zGKrS0NKcdd6b7TF3Ln8jYSz7bj+sDWPqRAhnsISdm91kiMAFAP56jta3JpWbkEy/M3UDFwmMwM3mXri24ENW6lyZ+UtiLwhDRiR2VYiYkIB8BJp9TOKufG/WRMay1lANjEeVD9i0LMFUO1RgX3uy3IwaVhSaNTyKaGGxowgLu4+WahMB0MlBHyXQcBlDmEwLe72Nps1IJtEdncAXINOhuV/nXNkV6FrE0TNwzol5kKhSw03SdNQGjSbTBC+ko6GNGLUpFQZL1J6U1fxmcv0s7kZ1bBbfrmOezUiYzV4iS7XwdPniOUT+nmzkIxSHThZR5gNTAP1KybromW/skPX7vboQ6WrMCvZtsPKhONGS/XZa5e+eGilpi/1gQ/rUQXoeT+AiJCsRpK7NoayKbSiAeAywZsXOWCQ+rDJNSpj5qALL4LCX5s1LCIRaO6pM50kB8LdWgpOKqxPzCLqRAneTfZmjmUJu5csn021LlUWDw6xOxuUYJ3iGkAiogOcLOfPCCjqBnQiEkTwJdYoRAtkp+u0liceoWbxMfuDUD0e+EwJgBeOmdYBGN/wfjAN2syvgZCAs+Oonaaf2XEyDobhq3MVcU7GjWehqU1E8R7ClPA7qBXFCjG5ua7ZWsAu81AZRL1vkmJPc8e3IEYEcjnwf57S19HQX2MQav1T+81bx0sdgVbUqo7AsIWhEkW5WrOzPjRH4sRVyJf12pBbVLmnPgfDJHvc7d+PGHxkthS88YL0T9zNhOcGMS1ak+VZzU7DtAHEQnSl2Rk0PFOZNcmP2gy/Hl6nytaJYg00ul1dZOxn80nau0pGu+Ve+jtqVFkvLov4WNkCam9Vv9Hhw/bqw/hFCEgGP43D7msgeWNgw+Ud6FWxTG9f4mp/0EU/3+qtuskqL68rWJONn8pdC9SydJnEcRb/JLfx62lg4j+PV4lMi2Qx03j+MASPbQczhUP1Xw=", /* 29 */2, /* 30 */undefined, /* 31 */"window", /* 32 */33702, /* 33 */60633, /* 34 */65304, /* 35 */3987, /* 36 */36331, /* 37 */21348, /* 38 */14130, /* 39 */15778, /* 40 */42590, /* 41 */12069, /* 42 */27176, /* 43 */10935, /* 44 */5213, /* 45 */2647, /* 46 */15248, /* 47 */28261, /* 48 */"length", /* 49 */"", /* 50 */"charCodeAt", /* 51 */8, /* 52 */"String", /* 53 */"fromCharCode", /* 54 */"atob", /* 55 */48075, /* 56 */10466, /* 57 */30760, /* 58 */23728, /* 59 */19831, /* 60 */19258, /* 61 */11415, /* 62 */10838, /* 63 */44126, /* 64 */37709, /* 65 */5779, /* 66 */1764, /* 67 */17096, /* 68 */5533, /* 69 */39222, /* 70 */50561, /* 71 */48231, /* 72 */39347, /* 73 */5543, /* 74 */23784, /* 75 */42522, /* 76 */194, /* 77 */2654435769, /* 78 */13, /* 79 */65535]; +var BYTECODE = [4, 25, 0, 26, 0, 0, 5, 27, 26, 0, 28, 1, 0, 5, 29, 28, 0, 30, 2, 0, 27, 31, 27, 30, 40, 31, 505, 0, 32, 3, 0, 5, 29, 32, 26, 33, 27, 29, 40, 33, 43, 39, 820, 0, 34, 4, 0, 11, 35, 29, 34, 5, 29, 35, 26, 36, 27, 29, 40, 36, 63, 39, 799, 0, 37, 5, 0, 11, 38, 29, 37, 5, 29, 38, 26, 39, 27, 29, 40, 39, 83, 39, 878, 0, 40, 6, 0, 12, 41, 29, 40, 5, 29, 41, 26, 42, 27, 29, 40, 42, 103, 39, 872, 0, 43, 7, 0, 12, 44, 29, 43, 5, 29, 44, 26, 45, 27, 29, 40, 45, 123, 39, 544, 0, 46, 8, 0, 12, 47, 29, 46, 5, 29, 47, 26, 48, 27, 29, 40, 48, 143, 39, 525, 0, 49, 9, 0, 11, 50, 29, 49, 5, 29, 50, 26, 51, 27, 29, 40, 51, 163, 39, 696, 0, 52, 10, 0, 12, 53, 29, 52, 5, 29, 53, 26, 54, 27, 29, 40, 54, 183, 39, 938, 0, 55, 11, 0, 11, 56, 29, 55, 5, 29, 56, 26, 57, 27, 29, 40, 57, 203, 39, 613, 0, 58, 12, 0, 12, 59, 29, 58, 5, 29, 59, 26, 60, 27, 29, 40, 60, 223, 39, 897, 0, 61, 13, 0, 11, 62, 29, 61, 5, 29, 62, 26, 63, 27, 29, 40, 63, 243, 39, 568, 0, 64, 14, 0, 12, 65, 29, 64, 5, 29, 65, 26, 66, 27, 29, 40, 66, 263, 39, 677, 0, 67, 15, 0, 11, 68, 29, 67, 5, 29, 68, 26, 69, 27, 29, 40, 69, 283, 39, 849, 0, 70, 16, 0, 12, 71, 29, 70, 5, 29, 71, 26, 72, 27, 29, 40, 72, 303, 39, 649, 0, 73, 17, 0, 11, 74, 29, 73, 5, 29, 74, 26, 75, 27, 29, 40, 75, 323, 39, 716, 0, 76, 18, 0, 12, 77, 29, 76, 5, 29, 77, 26, 78, 27, 29, 40, 78, 343, 39, 790, 0, 79, 19, 0, 11, 80, 29, 79, 5, 29, 80, 26, 81, 27, 29, 40, 81, 363, 39, 765, 0, 82, 20, 0, 11, 83, 29, 82, 5, 29, 83, 26, 84, 27, 29, 40, 84, 383, 39, 916, 0, 85, 21, 0, 12, 86, 29, 85, 5, 29, 86, 26, 87, 27, 29, 40, 87, 403, 39, 625, 0, 88, 22, 0, 11, 89, 29, 88, 5, 29, 89, 26, 90, 27, 29, 40, 90, 423, 39, 962, 0, 91, 23, 0, 11, 92, 29, 91, 5, 29, 92, 26, 93, 27, 29, 40, 93, 443, 39, 737, 0, 94, 24, 0, 12, 95, 29, 94, 5, 29, 95, 26, 96, 27, 29, 40, 96, 463, 39, 592, 0, 97, 25, 0, 11, 98, 29, 97, 5, 29, 98, 26, 99, 27, 29, 40, 99, 483, 39, 505, 0, 100, 26, 0, 12, 101, 29, 100, 5, 29, 101, 26, 102, 27, 29, 40, 102, 503, 39, 921, 39, 16, 59, 716, 4, 5, 916, 1, 22, 96446143, 1, 23, 214, 1, 103, 27301, 12, 27, 27, 103, 39, 16, 11, 104, 14, 3, 43, 105, 2, 13, 1, 104, 1, 103, 48237, 11, 27, 27, 103, 39, 16, 42, 9, 21, 4, 18, 22, 23, 24, 8, 10, 2, 9, 32, 11, 3, 1, 103, 38112, 11, 27, 27, 103, 39, 16, 1, 24, 4, 42, 7, 21, 4, 18, 22, 23, 24, 8, 8, 2, 7, 1, 103, 4723, 12, 27, 27, 103, 39, 16, 1, 27, 43486, 1, 22, 96446143, 1, 23, 214, 1, 24, 4, 1, 103, 38226, 12, 27, 27, 103, 39, 16, 5, 3, 15, 1, 103, 28757, 12, 27, 27, 103, 39, 16, 42, 12, 21, 4, 18, 22, 23, 24, 8, 13, 2, 12, 1, 22, 4016674594, 1, 103, 55015, 11, 27, 27, 103, 39, 16, 1, 103, 11387, 22, 103, 27, 103, 41, 103, 668, 1, 103, 33477, 12, 27, 27, 103, 39, 16, 1, 103, 8326, 12, 27, 27, 103, 39, 16, 43, 104, 2, 8, 1, 3, 55, 1, 4, 592, 1, 103, 19932, 11, 27, 27, 103, 39, 16, 5, 104, 3, 0, 104, 27, 0, 11, 15, 3, 104, 1, 103, 29339, 12, 27, 27, 103, 39, 16, 1, 27, 50142, 1, 22, 96446143, 1, 23, 214, 1, 24, 4, 1, 103, 26138, 12, 27, 27, 103, 39, 16, 1, 103, 45485, 22, 103, 27, 103, 41, 103, 756, 1, 103, 14397, 12, 27, 27, 103, 39, 16, 1, 103, 14397, 12, 27, 27, 103, 39, 16, 47, 19, 971, 1, 66, 0, 0, 0, 20, 28, 0, 42, 18, 19, 1, 20, 1, 103, 10748, 12, 27, 27, 103, 39, 16, 1, 103, 0, 11, 27, 27, 103, 39, 16, 47, 21, 1528, 4, 89, 0, 0, 4, 1, 48, 6, 0, 1, 103, 9232, 11, 27, 27, 103, 39, 16, 0, 104, 29, 0, 22, 105, 3, 104, 41, 105, 840, 1, 103, 3214, 11, 27, 27, 103, 39, 16, 1, 103, 60204, 11, 27, 27, 103, 39, 16, 1, 23, 222, 1, 24, 5, 42, 14, 21, 4, 18, 22, 23, 24, 1, 103, 48386, 12, 27, 27, 103, 39, 16, 0, 104, 30, 0, 45, 104, 5, 2, 6, 0, 104, 1, 0, 5, 3, 104, 1, 103, 25151, 12, 27, 27, 103, 39, 16, 2, 16, 31, 0, 1, 22, 2909690708, 1, 23, 229, 1, 103, 5235, 12, 27, 27, 103, 39, 16, 1, 27, 48703, 46, 5, 43, 104, 2, 10, 1, 11, 58, 4, 1, 103, 13413, 12, 27, 27, 103, 39, 16, 1, 24, 11, 42, 17, 21, 4, 18, 22, 23, 24, 9, 16, 17, 2, 1, 103, 25196, 11, 27, 27, 103, 39, 16, 1, 103, 13442, 12, 27, 27, 103, 39, 16, 4, 15, 0, 16, 32, 0, 5, 17, 16, 0, 18, 1, 0, 5, 19, 18, 0, 20, 33, 0, 27, 21, 17, 20, 40, 21, 1276, 0, 22, 34, 0, 5, 19, 22, 26, 23, 17, 19, 40, 23, 1014, 39, 1427, 0, 24, 35, 0, 12, 25, 19, 24, 5, 19, 25, 26, 26, 17, 19, 40, 26, 1034, 39, 1292, 0, 27, 36, 0, 12, 28, 19, 27, 5, 19, 28, 26, 29, 17, 19, 40, 29, 1054, 39, 1418, 0, 30, 37, 0, 11, 31, 19, 30, 5, 19, 31, 26, 32, 17, 19, 40, 32, 1074, 39, 1492, 0, 33, 38, 0, 12, 34, 19, 33, 5, 19, 34, 26, 35, 17, 19, 40, 35, 1094, 39, 1370, 0, 36, 39, 0, 11, 37, 19, 36, 5, 19, 37, 26, 38, 17, 19, 40, 38, 1114, 39, 1395, 0, 39, 40, 0, 12, 40, 19, 39, 5, 19, 40, 26, 41, 17, 19, 40, 41, 1134, 39, 1350, 0, 42, 41, 0, 11, 43, 19, 42, 5, 19, 43, 26, 44, 17, 19, 40, 44, 1154, 39, 1450, 0, 45, 42, 0, 11, 46, 19, 45, 5, 19, 46, 26, 47, 17, 19, 40, 47, 1174, 39, 1327, 0, 48, 43, 0, 12, 49, 19, 48, 5, 19, 49, 26, 50, 17, 19, 40, 50, 1194, 39, 1508, 0, 51, 44, 0, 11, 52, 19, 51, 5, 19, 52, 26, 53, 17, 19, 40, 53, 1214, 39, 1294, 0, 54, 45, 0, 11, 55, 19, 54, 5, 19, 55, 26, 56, 17, 19, 40, 56, 1234, 39, 1276, 0, 57, 46, 0, 11, 58, 19, 57, 5, 19, 58, 26, 59, 17, 19, 40, 59, 1254, 39, 1376, 0, 60, 47, 0, 12, 61, 19, 60, 5, 19, 61, 26, 62, 17, 19, 40, 62, 1274, 39, 1471, 39, 987, 11, 64, 4, 9, 5, 4, 64, 1, 63, 36170, 12, 17, 17, 63, 39, 987, 45, 4, 0, 64, 48, 0, 8, 65, 3, 64, 22, 64, 5, 65, 41, 64, 1318, 1, 63, 22402, 11, 17, 17, 63, 39, 987, 1, 63, 21454, 12, 17, 17, 63, 39, 987, 19, 64, 12, 14, 17, 65, 10, 64, 43, 9, 7, 8, 1, 65, 1, 63, 3075, 12, 17, 17, 63, 39, 987, 0, 64, 29, 0, 11, 65, 5, 64, 5, 5, 65, 1, 63, 33523, 11, 17, 17, 63, 39, 987, 0, 64, 30, 0, 45, 64, 5, 3, 6, 0, 64, 49, 0, 5, 4, 64, 1, 63, 10476, 12, 17, 17, 63, 39, 987, 0, 64, 50, 0, 8, 65, 3, 64, 43, 10, 3, 65, 1, 5, 1, 63, 19433, 12, 17, 17, 63, 39, 987, 1, 63, 36331, 11, 17, 17, 63, 39, 987, 11, 64, 5, 13, 43, 12, 3, 11, 1, 64, 0, 14, 51, 0, 1, 63, 20667, 12, 17, 17, 63, 39, 987, 2, 7, 52, 0, 0, 64, 53, 0, 8, 8, 7, 64, 1, 63, 30521, 11, 17, 17, 63, 39, 987, 0, 64, 50, 0, 8, 11, 3, 64, 0, 13, 27, 0, 1, 63, 36755, 11, 17, 17, 63, 39, 987, 0, 64, 1, 0, 5, 5, 64, 1, 63, 7419, 12, 17, 17, 63, 39, 987, 4, 2, 2, 64, 54, 0, 42, 6, 64, 1, 0, 1, 63, 23108, 11, 17, 17, 63, 39, 987, 4, 20, 0, 21, 55, 0, 5, 22, 21, 0, 23, 1, 0, 5, 24, 23, 0, 25, 56, 0, 27, 26, 22, 25, 40, 26, 1953, 0, 27, 57, 0, 5, 24, 27, 26, 28, 22, 24, 40, 28, 1571, 39, 2261, 0, 29, 58, 0, 11, 30, 24, 29, 5, 24, 30, 26, 31, 22, 24, 40, 31, 1591, 39, 2149, 0, 32, 59, 0, 12, 33, 24, 32, 5, 24, 33, 26, 34, 22, 24, 40, 34, 1611, 39, 1992, 0, 35, 60, 0, 11, 36, 24, 35, 5, 24, 36, 26, 37, 22, 24, 40, 37, 1631, 39, 2129, 0, 38, 61, 0, 11, 39, 24, 38, 5, 24, 39, 26, 40, 22, 24, 40, 40, 1651, 39, 2012, 0, 41, 62, 0, 12, 42, 24, 41, 5, 24, 42, 26, 43, 22, 24, 40, 43, 1671, 39, 2255, 0, 44, 63, 0, 12, 45, 24, 44, 5, 24, 45, 26, 46, 22, 24, 40, 46, 1691, 39, 2081, 0, 47, 64, 0, 11, 48, 24, 47, 5, 24, 48, 26, 49, 22, 24, 40, 49, 1711, 39, 2063, 0, 50, 65, 0, 11, 51, 24, 50, 5, 24, 51, 26, 52, 22, 24, 40, 52, 1731, 39, 2294, 0, 53, 66, 0, 12, 54, 24, 53, 5, 24, 54, 26, 55, 22, 24, 40, 55, 1751, 39, 2235, 0, 56, 67, 0, 12, 57, 24, 56, 5, 24, 57, 26, 58, 22, 24, 40, 58, 1771, 39, 2120, 0, 59, 68, 0, 11, 60, 24, 59, 5, 24, 60, 26, 61, 22, 24, 40, 61, 1791, 39, 2035, 0, 62, 69, 0, 12, 63, 24, 62, 5, 24, 63, 26, 64, 22, 24, 40, 64, 1811, 39, 1953, 0, 65, 70, 0, 11, 66, 24, 65, 5, 24, 66, 26, 67, 22, 24, 40, 67, 1831, 39, 1971, 0, 68, 71, 0, 12, 69, 24, 68, 5, 24, 69, 26, 70, 22, 24, 40, 70, 1851, 39, 2198, 0, 71, 72, 0, 11, 72, 24, 71, 5, 24, 72, 26, 73, 22, 24, 40, 73, 1871, 39, 2282, 0, 74, 73, 0, 12, 75, 24, 74, 5, 24, 75, 26, 76, 22, 24, 40, 76, 1891, 39, 1962, 0, 77, 74, 0, 11, 78, 24, 77, 5, 24, 78, 26, 79, 22, 24, 40, 79, 1911, 39, 2177, 0, 80, 75, 0, 12, 81, 24, 80, 5, 24, 81, 26, 82, 22, 24, 40, 82, 1931, 39, 2210, 0, 83, 76, 0, 11, 84, 24, 83, 5, 24, 84, 26, 85, 22, 24, 40, 85, 1951, 39, 2097, 39, 1544, 1, 86, 29455, 11, 22, 22, 86, 39, 1544, 1, 86, 18738, 12, 22, 22, 86, 39, 1544, 0, 87, 77, 0, 11, 9, 1, 87, 0, 10, 1, 0, 1, 86, 2049, 11, 22, 22, 86, 39, 1544, 5, 87, 7, 0, 87, 27, 0, 11, 19, 7, 87, 1, 86, 8325, 11, 22, 22, 86, 39, 1544, 8, 87, 0, 16, 11, 88, 2, 7, 43, 17, 0, 87, 1, 88, 1, 86, 46435, 12, 22, 22, 86, 39, 1544, 1, 86, 1143, 22, 86, 22, 86, 41, 86, 2054, 1, 86, 24803, 11, 22, 22, 86, 39, 1544, 1, 86, 21826, 12, 22, 22, 86, 39, 1544, 4, 5, 0, 87, 49, 0, 5, 6, 87, 1, 86, 37709, 12, 22, 22, 86, 39, 1544, 0, 87, 1, 0, 5, 7, 87, 1, 86, 8335, 11, 22, 22, 86, 39, 1544, 18, 87, 17, 8, 43, 88, 14, 15, 1, 87, 11, 18, 6, 88, 1, 86, 15260, 12, 22, 22, 86, 39, 1544, 1, 86, 13081, 11, 22, 22, 86, 39, 1544, 17, 87, 9, 10, 5, 1, 87, 0, 11, 78, 0, 1, 86, 23155, 12, 22, 22, 86, 39, 1544, 1, 86, 4865, 22, 86, 22, 86, 41, 86, 2168, 1, 86, 44122, 12, 22, 22, 86, 39, 1544, 1, 86, 634, 12, 22, 22, 86, 39, 1544, 0, 87, 53, 0, 8, 15, 14, 87, 0, 16, 50, 0, 1, 86, 4107, 11, 22, 22, 86, 39, 1544, 5, 6, 18, 1, 86, 31022, 11, 22, 22, 86, 39, 1544, 22, 87, 7, 3, 41, 87, 2226, 1, 86, 35153, 11, 22, 22, 86, 39, 1544, 1, 86, 33165, 11, 22, 22, 86, 39, 1544, 16, 87, 12, 13, 5, 8, 87, 2, 14, 52, 0, 1, 86, 9133, 11, 22, 22, 86, 39, 1544, 0, 87, 30, 0, 45, 87, 21, 87, 1, 11, 18, 12, 1, 87, 0, 13, 79, 0, 1, 86, 21330, 11, 22, 22, 86, 39, 1544, 5, 7, 19, 1, 86, 24281, 12, 22, 22, 86, 39, 1544, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 106; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/qualification.json new file mode 100644 index 00000000..447394f8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/qualification.json @@ -0,0 +1,83 @@ +{ + "id": "finallyContinuationStrings--stringConcealingControlFlowFlattening--2", + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "case": "finallyContinuationStrings", + "profile": "stringConcealingControlFlowFlattening", + "seed": 2, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 0, + 0, + "after0", + 1, + -1, + "after1" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 0, + 0, + "after0", + 1, + -1, + "after1" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 0, + 0, + "after0", + 1, + -1, + "after1" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/source.js new file mode 100644 index 00000000..b02f9dac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-finally-continuation/source.js @@ -0,0 +1,3 @@ +var out = []; +for (var i = 0; i < 2; i++) { try { out.push(i); } finally { out.push(-i); } out.push("after" + i); } +window.TEST_OUTPUT = out; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/encoded.js new file mode 100644 index 00000000..579f8d48 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */65160, /* 1 */0, /* 2 */52764, /* 3 */36909, /* 4 */16998, /* 5 */8786, /* 6 */17444, /* 7 */22016, /* 8 */39590, /* 9 */58915, /* 10 */8037, /* 11 */52323, /* 12 */1, /* 13 */"M1MJSbeJEuB+oct/dFsWyDEdLsmYM5pswtXRlBkkF6zfXbv4CQpK6usPxiAiMdEBqvC7c3iiCP2YO8x3Jll5DW6FaRjNDtX1Fs4pAGn7kBLt+AOH2zGKrS0NKcdd6b7TF3Ln8jYSz7bj+sDWPqRAhnsISdm91kiMAFAP56jta3JpWbkEy/M3UDFwmMwM3mXri24ENW6lyZ+UtiLwhDRiR2VYiYkIB8BJp9TOKufG/WRMay1lANjEeVD9i0LMFUO1RgX3uy3IwaVhSaNTyKaGGxowgLu4+WahMB0MlBHyXQcBlDmEwLe72Nps1IJtEdncAXINOhuV/nXNkV6FrE0TNwzol5kKhSw03SdNQGjSbTBC+ko6GNGLUpFQZL1J6U1fxmcv0s7kZ1bBbfrmOezUiYzV4iS7XwdPniOUT+nmzkIxSHThZR5gNTAP1KybromW/skPX7vboQ6WrMCvZtsPKhONGS/XZa5e+eGilpi/1gQ/rUQXoeT+AiJCsRpK7NoayKbSiAeAywZsXOWCQ+rDJNSpj5qALL4LCX5s1LCIRaO6pM50kB8LdWgpOKqVPwCLiRAbeavhPlmEMAjPh0P/+OlwN9maOZQm7lyyfexuUYJurUhsoRAtkp+u0liceoWbxMfuDUD0e+EwJgBeOmdYBGN/wfjAN2syvgZCAs+Oonaaf2XEyDobhq3MVcU7GjWehqU1E8R7ClPA7qBXFCjG5ua7ZWsAu81AZRL1vkmJPc8e3IEYEcjnwf57S19HQX2MQav1T+81bx0sdgVbUqo7AsIWhEkW5WrOzPjRH4sRVyJf12pBbVLmnPgfDJHvc7d+PGHxkthS88YL0T9zNhOcGMS1ak+VZzU7DtAHEQnSl2Rk0PFOZNcmP2gy/Hl6nytaJYg00ul1dZOxn80nau0pGu+Ve+jtqVFkvLov4WNkCam9Vv9Hhw/bqw/hFCEgGP43D7msgeWNgw+Ud6FWxTG9f4mp/0EU/3+qtuskqL68rWJONn8pdC9SydJnEcRb/JLfx62lgw==", /* 14 */undefined, /* 15 */"window", /* 16 */25205, /* 17 */34447, /* 18 */18702, /* 19 */23674, /* 20 */3452, /* 21 */20623, /* 22 */17020, /* 23 */7213, /* 24 */33141, /* 25 */3312, /* 26 */1593, /* 27 */11512, /* 28 */7203, /* 29 */23361, /* 30 */53588, /* 31 */6388, /* 32 */14793, /* 33 */10020, /* 34 */9050, /* 35 */23890, /* 36 */"", /* 37 */"charCodeAt", /* 38 */8, /* 39 */2, /* 40 */"String", /* 41 */"fromCharCode", /* 42 */"atob", /* 43 */"length", /* 44 */61317, /* 45 */50483, /* 46 */39952, /* 47 */20681, /* 48 */51513, /* 49 */350, /* 50 */22460, /* 51 */13407, /* 52 */1697, /* 53 */21348, /* 54 */31824, /* 55 */2415, /* 56 */6079, /* 57 */3987, /* 58 */29113, /* 59 */15778, /* 60 */15654, /* 61 */9234, /* 62 */13013, /* 63 */13, /* 64 */65535, /* 65 */2654435769]; +var BYTECODE = [4, 14, 0, 15, 0, 0, 5, 16, 15, 0, 17, 1, 0, 5, 18, 17, 0, 19, 2, 0, 27, 20, 16, 19, 40, 20, 205, 0, 21, 3, 0, 5, 18, 21, 26, 22, 16, 18, 40, 22, 43, 39, 205, 0, 23, 4, 0, 12, 24, 18, 23, 5, 18, 24, 26, 25, 16, 18, 40, 25, 63, 39, 298, 0, 26, 5, 0, 11, 27, 18, 26, 5, 18, 27, 26, 28, 16, 18, 40, 28, 83, 39, 322, 0, 29, 6, 0, 11, 30, 18, 29, 5, 18, 30, 26, 31, 16, 18, 40, 31, 103, 39, 227, 0, 32, 7, 0, 12, 33, 18, 32, 5, 18, 33, 26, 34, 16, 18, 40, 34, 123, 39, 354, 0, 35, 8, 0, 11, 36, 18, 35, 5, 18, 36, 26, 37, 16, 18, 40, 37, 143, 39, 331, 0, 38, 9, 0, 12, 39, 18, 38, 5, 18, 39, 26, 40, 16, 18, 40, 40, 163, 39, 244, 0, 41, 10, 0, 11, 42, 18, 41, 5, 18, 42, 26, 43, 16, 18, 40, 43, 183, 39, 292, 0, 44, 11, 0, 11, 45, 18, 44, 5, 18, 45, 26, 46, 16, 18, 40, 46, 203, 39, 267, 39, 16, 47, 10, 1084, 4, 80, 0, 0, 4, 1, 0, 3, 12, 0, 1, 47, 16998, 12, 16, 16, 47, 39, 16, 48, 48, 1, 6, 9, 4, 5, 48, 1, 47, 33304, 12, 16, 16, 47, 39, 16, 1, 12, 229, 1, 13, 1, 42, 6, 10, 4, 7, 11, 12, 13, 1, 47, 41341, 11, 16, 16, 47, 39, 16, 47, 8, 373, 1, 78, 0, 0, 0, 9, 13, 0, 42, 7, 8, 1, 9, 1, 47, 28251, 12, 16, 16, 47, 39, 16, 0, 48, 14, 0, 45, 48, 5, 2, 3, 41, 2, 313, 1, 47, 7074, 12, 16, 16, 47, 39, 16, 1, 47, 4214, 11, 16, 16, 47, 39, 16, 1, 47, 8212, 11, 16, 16, 47, 39, 16, 1, 13, 11, 42, 5, 10, 4, 7, 11, 12, 13, 1, 11, 4016674594, 1, 47, 58915, 12, 16, 16, 47, 39, 16, 2, 4, 15, 0, 1, 11, 96446143, 1, 12, 214, 1, 47, 39590, 11, 16, 16, 47, 39, 16, 4, 15, 0, 16, 16, 0, 5, 17, 16, 0, 18, 1, 0, 5, 19, 18, 0, 20, 17, 0, 27, 21, 17, 20, 40, 21, 758, 0, 22, 18, 0, 5, 19, 22, 26, 23, 17, 19, 40, 23, 416, 39, 821, 0, 24, 19, 0, 11, 25, 19, 24, 5, 19, 25, 26, 26, 17, 19, 40, 26, 436, 39, 892, 0, 27, 20, 0, 11, 28, 19, 27, 5, 19, 28, 26, 29, 17, 19, 40, 29, 456, 39, 1035, 0, 30, 21, 0, 12, 31, 19, 30, 5, 19, 31, 26, 32, 17, 19, 40, 32, 476, 39, 987, 0, 33, 22, 0, 11, 34, 19, 33, 5, 19, 34, 26, 35, 17, 19, 40, 35, 496, 39, 944, 0, 36, 23, 0, 12, 37, 19, 36, 5, 19, 37, 26, 38, 17, 19, 40, 38, 516, 39, 864, 0, 39, 24, 0, 12, 40, 19, 39, 5, 19, 40, 26, 41, 17, 19, 40, 41, 536, 39, 1051, 0, 42, 25, 0, 11, 43, 19, 42, 5, 19, 43, 26, 44, 17, 19, 40, 44, 556, 39, 777, 0, 45, 26, 0, 11, 46, 19, 45, 5, 19, 46, 26, 47, 17, 19, 40, 47, 576, 39, 844, 0, 48, 27, 0, 11, 49, 19, 48, 5, 19, 49, 26, 50, 17, 19, 40, 50, 596, 39, 798, 0, 51, 28, 0, 11, 52, 19, 51, 5, 19, 52, 26, 53, 17, 19, 40, 53, 616, 39, 921, 0, 54, 29, 0, 12, 55, 19, 54, 5, 19, 55, 26, 56, 17, 19, 40, 56, 636, 39, 910, 0, 57, 30, 0, 11, 58, 19, 57, 5, 19, 58, 26, 59, 17, 19, 40, 59, 656, 39, 1007, 0, 60, 31, 0, 12, 61, 19, 60, 5, 19, 61, 26, 62, 17, 19, 40, 62, 676, 39, 919, 0, 63, 32, 0, 12, 64, 19, 63, 5, 19, 64, 26, 65, 17, 19, 40, 65, 696, 39, 901, 0, 66, 33, 0, 11, 67, 19, 66, 5, 19, 67, 26, 68, 17, 19, 40, 68, 716, 39, 981, 0, 69, 34, 0, 12, 70, 19, 69, 5, 19, 70, 26, 71, 17, 19, 40, 71, 736, 39, 758, 0, 72, 35, 0, 12, 73, 19, 72, 5, 19, 73, 26, 74, 17, 19, 40, 74, 756, 39, 965, 39, 389, 5, 3, 6, 0, 76, 36, 0, 5, 4, 76, 1, 75, 10321, 11, 17, 17, 75, 39, 389, 0, 76, 37, 0, 8, 11, 3, 76, 0, 13, 12, 0, 1, 75, 13105, 11, 17, 17, 75, 39, 389, 11, 76, 5, 13, 43, 12, 3, 11, 1, 76, 0, 14, 38, 0, 1, 75, 7203, 11, 17, 17, 75, 39, 389, 0, 76, 37, 0, 8, 77, 3, 76, 43, 10, 3, 77, 1, 5, 1, 75, 13519, 12, 17, 17, 75, 39, 389, 0, 76, 39, 0, 11, 77, 5, 76, 5, 5, 77, 1, 75, 4905, 12, 17, 17, 75, 39, 389, 1, 75, 49631, 22, 75, 17, 75, 41, 75, 883, 1, 75, 9521, 12, 17, 17, 75, 39, 389, 1, 75, 23395, 12, 17, 17, 75, 39, 389, 1, 75, 24088, 12, 17, 17, 75, 39, 389, 1, 75, 9046, 12, 17, 17, 75, 39, 389, 1, 75, 4646, 11, 17, 17, 75, 39, 389, 45, 4, 19, 76, 12, 14, 17, 77, 10, 76, 43, 9, 7, 8, 1, 77, 1, 75, 13874, 12, 17, 17, 75, 39, 389, 2, 7, 40, 0, 0, 76, 41, 0, 8, 8, 7, 76, 1, 75, 23523, 12, 17, 17, 75, 39, 389, 11, 76, 4, 9, 5, 4, 76, 1, 75, 4841, 12, 17, 17, 75, 39, 389, 0, 76, 14, 0, 45, 76, 4, 2, 2, 76, 42, 0, 42, 6, 76, 1, 0, 1, 75, 10302, 11, 17, 17, 75, 39, 389, 1, 75, 38915, 22, 75, 17, 75, 41, 75, 1026, 1, 75, 48942, 12, 17, 17, 75, 39, 389, 1, 75, 50535, 12, 17, 17, 75, 39, 389, 0, 76, 1, 0, 5, 5, 76, 1, 75, 43957, 12, 17, 17, 75, 39, 389, 0, 76, 43, 0, 8, 77, 3, 76, 22, 76, 5, 77, 41, 76, 1075, 1, 75, 47459, 11, 17, 17, 75, 39, 389, 1, 75, 40354, 11, 17, 17, 75, 39, 389, 4, 20, 0, 21, 44, 0, 5, 22, 21, 0, 23, 1, 0, 5, 24, 23, 0, 25, 45, 0, 27, 26, 22, 25, 40, 26, 1449, 0, 27, 46, 0, 5, 24, 27, 26, 28, 22, 24, 40, 28, 1127, 39, 1612, 0, 29, 47, 0, 11, 30, 24, 29, 5, 24, 30, 26, 31, 22, 24, 40, 31, 1147, 39, 1550, 0, 32, 48, 0, 12, 33, 24, 32, 5, 24, 33, 26, 34, 22, 24, 40, 34, 1167, 39, 1479, 0, 35, 49, 0, 12, 36, 24, 35, 5, 24, 36, 26, 37, 22, 24, 40, 37, 1187, 39, 1570, 0, 38, 50, 0, 11, 39, 24, 38, 5, 24, 39, 26, 40, 22, 24, 40, 40, 1207, 39, 1500, 0, 41, 51, 0, 11, 42, 24, 41, 5, 24, 42, 26, 43, 22, 24, 40, 43, 1227, 39, 1528, 0, 44, 52, 0, 11, 45, 24, 44, 5, 24, 45, 26, 46, 22, 24, 40, 46, 1247, 39, 1701, 0, 47, 53, 0, 12, 48, 24, 47, 5, 24, 48, 26, 49, 22, 24, 40, 49, 1267, 39, 1656, 0, 50, 54, 0, 11, 51, 24, 50, 5, 24, 51, 26, 52, 22, 24, 40, 52, 1287, 39, 1635, 0, 53, 55, 0, 11, 54, 24, 53, 5, 24, 54, 26, 55, 22, 24, 40, 55, 1307, 39, 1491, 0, 56, 56, 0, 11, 57, 24, 56, 5, 24, 57, 26, 58, 22, 24, 40, 58, 1327, 39, 1685, 0, 59, 57, 0, 12, 60, 24, 59, 5, 24, 60, 26, 61, 22, 24, 40, 61, 1347, 39, 1449, 0, 62, 58, 0, 12, 63, 24, 62, 5, 24, 63, 26, 64, 22, 24, 40, 64, 1367, 39, 1721, 0, 65, 59, 0, 11, 66, 24, 65, 5, 24, 66, 26, 67, 22, 24, 40, 67, 1387, 39, 1548, 0, 68, 60, 0, 12, 69, 24, 68, 5, 24, 69, 26, 70, 22, 24, 40, 70, 1407, 39, 1467, 0, 71, 61, 0, 11, 72, 24, 71, 5, 24, 72, 26, 73, 22, 24, 40, 73, 1427, 39, 1591, 0, 74, 62, 0, 12, 75, 24, 74, 5, 24, 75, 26, 76, 22, 24, 40, 76, 1447, 39, 1679, 39, 1100, 4, 5, 0, 78, 36, 0, 5, 6, 78, 1, 77, 3987, 11, 22, 22, 77, 39, 1100, 5, 6, 18, 1, 77, 14006, 11, 22, 22, 77, 39, 1100, 5, 7, 19, 1, 77, 23084, 11, 22, 22, 77, 39, 1100, 1, 77, 2092, 11, 22, 22, 77, 39, 1100, 1, 77, 10466, 22, 77, 22, 77, 41, 77, 1519, 1, 77, 34074, 11, 22, 22, 77, 39, 1100, 1, 77, 34074, 11, 22, 22, 77, 39, 1100, 17, 78, 9, 10, 5, 1, 78, 0, 11, 63, 0, 1, 77, 3075, 12, 22, 22, 77, 39, 1100, 45, 6, 16, 78, 12, 13, 5, 8, 78, 2, 14, 40, 0, 1, 77, 51863, 12, 22, 22, 77, 39, 1100, 0, 78, 41, 0, 8, 15, 14, 78, 0, 16, 37, 0, 1, 77, 31182, 11, 22, 22, 77, 39, 1100, 21, 78, 1, 11, 18, 12, 1, 78, 0, 13, 64, 0, 1, 77, 19071, 11, 22, 22, 77, 39, 1100, 8, 78, 0, 16, 11, 79, 2, 7, 43, 17, 0, 78, 1, 79, 1, 77, 14966, 12, 22, 22, 77, 39, 1100, 0, 78, 65, 0, 11, 9, 1, 78, 0, 10, 1, 0, 1, 77, 12173, 12, 22, 22, 77, 39, 1100, 18, 78, 17, 8, 43, 79, 14, 15, 1, 78, 11, 18, 6, 79, 1, 77, 7342, 11, 22, 22, 77, 39, 1100, 0, 78, 14, 0, 45, 78, 0, 78, 1, 0, 5, 7, 78, 1, 77, 33100, 12, 22, 22, 77, 39, 1100, 5, 78, 7, 0, 78, 12, 0, 11, 19, 7, 78, 1, 77, 37214, 12, 22, 22, 77, 39, 1100, 22, 78, 7, 3, 41, 78, 1737, 1, 77, 15778, 11, 22, 22, 77, 39, 1100, 1, 77, 24606, 11, 22, 22, 77, 39, 1100]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 49; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/qualification.json new file mode 100644 index 00000000..a34a7b77 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/qualification.json @@ -0,0 +1,68 @@ +{ + "id": "mainBranchStrings--stringConcealingControlFlowFlattening--2", + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "case": "mainBranchStrings", + "profile": "stringConcealingControlFlowFlattening", + "seed": 2, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "a" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "a" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "a" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/source.js new file mode 100644 index 00000000..ae645bd2 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff-main-branch/source.js @@ -0,0 +1 @@ +var x = 1; if (x) window.TEST_OUTPUT = ["a"]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff/encoded.js new file mode 100644 index 00000000..97014122 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"7jOUI06henf6mkE96oNQf29yC4wNotxdlCxv0YvCvlcJJauu8eEU/WC6dX1TDl9uCrK6NhqhWzLzLSyWOM1FBwMzRZoXcuY0c6WhboBqtzogoXjzszaFFCnj3mii+fDimus6QifzVwjMqkObETFqTcEOQ5hV5K1ygmSg6qIdQz8h7VdgZ7nnbBI7S58NCWQRk2ExW8mPctzVMzqy6FttQnmaTyU2Ey34SbpErI+zV6mL8XuPBlz8U0sTm1Iwx0gzpIgm4d5IwR/UtScFThsic+xczeU3HAPxJ82cr2opL6K8kSStMzq3psQLEX8UqCw3Ua1fBcv7sZGyGrfqcEPlQ/zvooIy5h821/mw2072myts6fTtzhILilWMJyJB6coahj6yS5tS8Wg+5B5a/7S1QVPk3A1JaOXCWv7yraUUGSucGKImL0jPDhSsPZl9rQFBEJ2+jKY67XGke1nZlvCv1caSJOzh4/P0UMyNI26xpYxnijNAqYQ+ason7F9kTHwn9hoBPsTNs4dM0ctzSGXVXpojhL4e3JDVA1kaXqhaWWKyrL5PqDXiMjSvp2Is9bxNCt2RRgVLY99qaxzlkB2IHchg9LDU4k+rSz9/q2wh52VSP1yJtwFV/pJstD0YhZkKF/+9a2a/QhjnKFXVoPrAG+tjH16Jo6u6opJFq4XD2VirkrGMLyqDBVRkdwheW9EKH+UmmigNe4SRgD0eCUHzgRUTldc17fMclHjdMQbpr4EJRAWH/zP0mui89WCUQXH4oujqQhRZ3CLWXLTR9JUV3vU9h5qsn+R/jJWglVPZuRyTc5kAa7x2WJfUL7TZ6abFo5OODfGNYynuJTO+y76C3NED2bMdcG50NTf0sD4lJgaklndQ3seQ03XFaKbVJLmuWBGSqrmNPTWfahiLLRzPQN+YiHZclbjkmiRyrLyhJ4MBJLMMQRTv0H6cL4s=", /* 1 */"window", /* 2 */1, /* 3 */3, /* 4 */undefined, /* 5 */36913, /* 6 */0, /* 7 */32320, /* 8 */15391, /* 9 */21522, /* 10 */32218, /* 11 */1664, /* 12 */13869, /* 13 */22544, /* 14 */28760, /* 15 */51249, /* 16 */28541, /* 17 */2342, /* 18 */21539, /* 19 */48310, /* 20 */2076, /* 21 */3527, /* 22 */51604, /* 23 */2, /* 24 */2494, /* 25 */60240, /* 26 */17946, /* 27 */36415, /* 28 */51039, /* 29 */18968, /* 30 */35749, /* 31 */39845, /* 32 */15700, /* 33 */9529, /* 34 */44207, /* 35 */3813, /* 36 */603, /* 37 */52476, /* 38 */3582, /* 39 */24368, /* 40 */16464, /* 41 */20184, /* 42 */"atob", /* 43 */"", /* 44 */"charCodeAt", /* 45 */8, /* 46 */"String", /* 47 */"fromCharCode", /* 48 */"length", /* 49 */40060, /* 50 */9035, /* 51 */51419, /* 52 */993, /* 53 */537, /* 54 */13223, /* 55 */13984, /* 56 */42110, /* 57 */4464, /* 58 */3519, /* 59 */37378, /* 60 */4588, /* 61 */31602, /* 62 */53598, /* 63 */33622, /* 64 */25939, /* 65 */24861, /* 66 */9415, /* 67 */23639, /* 68 */13903, /* 69 */65535, /* 70 */13, /* 71 */2654435769]; +var BYTECODE = [47, 5, 649, 1, 72, 0, 0, 0, 6, 0, 0, 42, 4, 5, 1, 6, 47, 7, 1283, 4, 83, 0, 0, 4, 1, 47, 11, 91, 1, 65, 1, 0, 1, 4, 5, 2, 11, 2, 11, 1, 0, 1, 8, 3138644866, 1, 9, 212, 1, 10, 11, 42, 3, 7, 4, 4, 8, 9, 10, 0, 12, 2, 0, 42, 13, 2, 1, 12, 0, 12, 3, 0, 42, 14, 2, 1, 12, 48, 12, 2, 13, 14, 9, 11, 3, 12, 0, 11, 4, 0, 45, 11, 4, 11, 0, 12, 5, 0, 5, 13, 12, 0, 14, 6, 0, 5, 15, 14, 0, 16, 7, 0, 27, 17, 13, 16, 40, 17, 416, 0, 18, 8, 0, 5, 15, 18, 26, 19, 13, 15, 40, 19, 134, 39, 474, 0, 20, 9, 0, 11, 21, 15, 20, 5, 15, 21, 26, 22, 13, 15, 40, 22, 154, 39, 453, 0, 23, 10, 0, 12, 24, 15, 23, 5, 15, 24, 26, 25, 13, 15, 40, 25, 174, 39, 574, 0, 26, 11, 0, 12, 27, 15, 26, 5, 15, 27, 26, 28, 13, 15, 40, 28, 194, 39, 536, 0, 29, 12, 0, 11, 30, 15, 29, 5, 15, 30, 26, 31, 13, 15, 40, 31, 214, 39, 435, 0, 32, 13, 0, 11, 33, 15, 32, 5, 15, 33, 26, 34, 13, 15, 40, 34, 234, 39, 416, 0, 35, 14, 0, 12, 36, 15, 35, 5, 15, 36, 26, 37, 13, 15, 40, 37, 254, 39, 631, 0, 38, 15, 0, 11, 39, 15, 38, 5, 15, 39, 26, 40, 13, 15, 40, 40, 274, 39, 499, 0, 41, 16, 0, 12, 42, 15, 41, 5, 15, 42, 26, 43, 13, 15, 40, 43, 294, 39, 489, 0, 44, 17, 0, 11, 45, 15, 44, 5, 15, 45, 26, 46, 13, 15, 40, 46, 314, 39, 584, 0, 47, 18, 0, 11, 48, 15, 47, 5, 15, 48, 26, 49, 13, 15, 40, 49, 334, 39, 613, 0, 50, 19, 0, 12, 51, 15, 50, 5, 15, 51, 26, 52, 13, 15, 40, 52, 354, 39, 565, 0, 53, 20, 0, 11, 54, 15, 53, 5, 15, 54, 26, 55, 13, 15, 40, 55, 374, 39, 527, 0, 56, 21, 0, 12, 57, 15, 56, 5, 15, 57, 26, 58, 13, 15, 40, 58, 394, 39, 426, 0, 59, 22, 0, 11, 60, 15, 59, 5, 15, 60, 26, 61, 13, 15, 40, 61, 414, 39, 480, 39, 107, 42, 3, 7, 4, 6, 8, 9, 10, 45, 3, 1, 62, 9388, 11, 13, 13, 62, 39, 107, 1, 8, 1991122513, 1, 9, 232, 1, 10, 3, 1, 62, 12205, 12, 13, 13, 62, 39, 107, 3, 6, 0, 47, 7, 1283, 4, 83, 0, 0, 4, 2, 1, 62, 33882, 12, 13, 13, 62, 39, 107, 0, 63, 4, 0, 45, 63, 1, 62, 48432, 12, 13, 13, 62, 39, 107, 42, 5, 7, 4, 6, 8, 9, 10, 45, 5, 1, 62, 18759, 22, 62, 13, 62, 41, 62, 518, 1, 62, 28541, 12, 13, 13, 62, 39, 107, 1, 62, 2817, 12, 13, 13, 62, 39, 107, 1, 62, 48077, 11, 13, 13, 62, 39, 107, 0, 63, 2, 0, 26, 64, 0, 63, 41, 64, 556, 1, 62, 32703, 11, 13, 13, 62, 39, 107, 1, 62, 56085, 11, 13, 13, 62, 39, 107, 1, 62, 48310, 11, 13, 13, 62, 39, 107, 42, 4, 7, 4, 6, 8, 9, 10, 45, 4, 0, 63, 23, 0, 26, 64, 0, 63, 41, 64, 604, 1, 62, 26771, 12, 13, 13, 62, 39, 107, 1, 62, 28222, 12, 13, 13, 62, 39, 107, 1, 8, 1851056128, 1, 9, 237, 1, 10, 5, 1, 62, 23881, 12, 13, 13, 62, 39, 107, 1, 8, 427924783, 1, 9, 227, 1, 10, 3, 1, 62, 28760, 11, 13, 13, 62, 39, 107, 4, 15, 0, 16, 24, 0, 5, 17, 16, 0, 18, 6, 0, 5, 19, 18, 0, 20, 25, 0, 27, 21, 17, 20, 40, 21, 994, 0, 22, 26, 0, 5, 19, 22, 26, 23, 17, 19, 40, 23, 692, 39, 1157, 0, 24, 27, 0, 11, 25, 19, 24, 5, 19, 25, 26, 26, 17, 19, 40, 26, 712, 39, 1208, 0, 27, 28, 0, 12, 28, 19, 27, 5, 19, 28, 26, 29, 17, 19, 40, 29, 732, 39, 1121, 0, 30, 29, 0, 11, 31, 19, 30, 5, 19, 31, 26, 32, 17, 19, 40, 32, 752, 39, 1092, 0, 33, 30, 0, 11, 34, 19, 33, 5, 19, 34, 26, 35, 17, 19, 40, 35, 772, 39, 1035, 0, 36, 31, 0, 12, 37, 19, 36, 5, 19, 37, 26, 38, 17, 19, 40, 38, 792, 39, 1076, 0, 39, 32, 0, 12, 40, 19, 39, 5, 19, 40, 26, 41, 17, 19, 40, 41, 812, 39, 994, 0, 42, 33, 0, 11, 43, 19, 42, 5, 19, 43, 26, 44, 17, 19, 40, 44, 832, 39, 1262, 0, 45, 34, 0, 11, 46, 19, 45, 5, 19, 46, 26, 47, 17, 19, 40, 47, 852, 39, 1185, 0, 48, 35, 0, 11, 49, 19, 48, 5, 19, 49, 26, 50, 17, 19, 40, 50, 872, 39, 1137, 0, 51, 36, 0, 12, 52, 19, 51, 5, 19, 52, 26, 53, 17, 19, 40, 53, 892, 39, 1044, 0, 54, 37, 0, 12, 55, 19, 54, 5, 19, 55, 26, 56, 17, 19, 40, 56, 912, 39, 1033, 0, 57, 38, 0, 12, 58, 19, 57, 5, 19, 58, 26, 59, 17, 19, 40, 59, 932, 39, 1014, 0, 60, 39, 0, 11, 61, 19, 60, 5, 19, 61, 26, 62, 17, 19, 40, 62, 952, 39, 1053, 0, 63, 40, 0, 11, 64, 19, 63, 5, 19, 64, 26, 65, 17, 19, 40, 65, 972, 39, 1115, 0, 66, 41, 0, 11, 67, 19, 66, 5, 19, 67, 26, 68, 17, 19, 40, 68, 992, 39, 1229, 39, 665, 4, 2, 2, 70, 42, 0, 42, 6, 70, 1, 0, 1, 69, 888, 11, 17, 17, 69, 39, 665, 5, 3, 6, 0, 70, 43, 0, 5, 4, 70, 1, 69, 60, 12, 17, 17, 69, 39, 665, 45, 4, 1, 69, 55545, 12, 17, 17, 69, 39, 665, 1, 69, 3210, 12, 17, 17, 69, 39, 665, 19, 70, 12, 14, 17, 71, 10, 70, 43, 9, 7, 8, 1, 71, 1, 69, 9556, 12, 17, 17, 69, 39, 665, 11, 70, 4, 9, 5, 4, 70, 1, 69, 41849, 11, 17, 17, 69, 39, 665, 0, 70, 44, 0, 8, 71, 3, 70, 43, 10, 3, 71, 1, 5, 1, 69, 10267, 12, 17, 17, 69, 39, 665, 0, 70, 4, 0, 45, 70, 0, 70, 6, 0, 5, 5, 70, 1, 69, 61076, 11, 17, 17, 69, 39, 665, 0, 70, 23, 0, 11, 71, 5, 70, 5, 5, 71, 1, 69, 4355, 11, 17, 17, 69, 39, 665, 1, 69, 31012, 22, 69, 17, 69, 41, 69, 1176, 1, 69, 42097, 11, 17, 17, 69, 39, 665, 1, 69, 41494, 11, 17, 17, 69, 39, 665, 11, 70, 5, 13, 43, 12, 3, 11, 1, 70, 0, 14, 45, 0, 1, 69, 28480, 12, 17, 17, 69, 39, 665, 2, 7, 46, 0, 0, 70, 47, 0, 8, 8, 7, 70, 1, 69, 32071, 12, 17, 17, 69, 39, 665, 0, 70, 48, 0, 8, 71, 3, 70, 22, 70, 5, 71, 41, 70, 1253, 1, 69, 57434, 12, 17, 17, 69, 39, 665, 1, 69, 10037, 12, 17, 17, 69, 39, 665, 0, 70, 44, 0, 8, 11, 3, 70, 0, 13, 2, 0, 1, 69, 44207, 11, 17, 17, 69, 39, 665, 4, 20, 0, 21, 49, 0, 5, 22, 21, 0, 23, 6, 0, 5, 24, 23, 0, 25, 50, 0, 27, 26, 22, 25, 40, 26, 1668, 0, 27, 51, 0, 5, 24, 27, 26, 28, 22, 24, 40, 28, 1326, 39, 1937, 0, 29, 52, 0, 12, 30, 24, 29, 5, 24, 30, 26, 31, 22, 24, 40, 31, 1346, 39, 1828, 0, 32, 53, 0, 11, 33, 24, 32, 5, 24, 33, 26, 34, 22, 24, 40, 34, 1366, 39, 1751, 0, 35, 54, 0, 12, 36, 24, 35, 5, 24, 36, 26, 37, 22, 24, 40, 37, 1386, 39, 1776, 0, 38, 55, 0, 11, 39, 24, 38, 5, 24, 39, 26, 40, 22, 24, 40, 40, 1406, 39, 1728, 0, 41, 56, 0, 12, 42, 24, 41, 5, 24, 42, 26, 43, 22, 24, 40, 43, 1426, 39, 1953, 0, 44, 57, 0, 11, 45, 24, 44, 5, 24, 45, 26, 46, 22, 24, 40, 46, 1446, 39, 1816, 0, 47, 58, 0, 12, 48, 24, 47, 5, 24, 48, 26, 49, 22, 24, 40, 49, 1466, 39, 1978, 0, 50, 59, 0, 11, 51, 24, 50, 5, 24, 51, 26, 52, 22, 24, 40, 52, 1486, 39, 1774, 0, 53, 60, 0, 12, 54, 24, 53, 5, 24, 54, 26, 55, 22, 24, 40, 55, 1506, 39, 1796, 0, 56, 61, 0, 12, 57, 24, 56, 5, 24, 57, 26, 58, 22, 24, 40, 58, 1526, 39, 1984, 0, 59, 62, 0, 11, 60, 24, 59, 5, 24, 60, 26, 61, 22, 24, 40, 61, 1546, 39, 1925, 0, 62, 63, 0, 12, 63, 24, 62, 5, 24, 63, 26, 64, 22, 24, 40, 64, 1566, 39, 1905, 0, 65, 64, 0, 12, 66, 24, 65, 5, 24, 66, 26, 67, 22, 24, 40, 67, 1586, 39, 1668, 0, 68, 65, 0, 11, 69, 24, 68, 5, 24, 69, 26, 70, 22, 24, 40, 70, 1606, 39, 1849, 0, 71, 66, 0, 11, 72, 24, 71, 5, 24, 72, 26, 73, 22, 24, 40, 73, 1626, 39, 1710, 0, 74, 67, 0, 11, 75, 24, 74, 5, 24, 75, 26, 76, 22, 24, 40, 76, 1646, 39, 1689, 0, 77, 68, 0, 12, 78, 24, 77, 5, 24, 78, 26, 79, 22, 24, 40, 79, 1666, 39, 1877, 39, 1299, 0, 81, 47, 0, 8, 15, 14, 81, 0, 16, 44, 0, 1, 80, 45940, 11, 22, 22, 80, 39, 1299, 21, 81, 1, 11, 18, 12, 1, 81, 0, 13, 69, 0, 1, 80, 20350, 12, 22, 22, 80, 39, 1299, 4, 5, 0, 81, 43, 0, 5, 6, 81, 1, 80, 11359, 11, 22, 22, 80, 39, 1299, 8, 81, 0, 16, 11, 82, 2, 7, 43, 17, 0, 81, 1, 82, 1, 80, 761, 12, 22, 22, 80, 39, 1299, 18, 81, 17, 8, 43, 82, 14, 15, 1, 81, 11, 18, 6, 82, 1, 80, 14382, 11, 22, 22, 80, 39, 1299, 45, 6, 17, 81, 9, 10, 5, 1, 81, 0, 11, 70, 0, 1, 80, 25959, 11, 22, 22, 80, 39, 1299, 16, 81, 12, 13, 5, 8, 81, 2, 14, 46, 0, 1, 80, 37565, 12, 22, 22, 80, 39, 1299, 5, 7, 19, 1, 80, 4464, 12, 22, 22, 80, 39, 1299, 0, 81, 71, 0, 11, 9, 1, 81, 0, 10, 6, 0, 1, 80, 12686, 12, 22, 22, 80, 39, 1299, 1, 80, 24043, 22, 80, 22, 80, 41, 80, 1868, 1, 80, 19781, 11, 22, 22, 80, 39, 1299, 1, 80, 12704, 11, 22, 22, 80, 39, 1299, 1, 80, 56792, 22, 80, 22, 80, 41, 80, 1896, 1, 80, 19151, 12, 22, 22, 80, 39, 1299, 1, 80, 15549, 11, 22, 22, 80, 39, 1299, 5, 81, 7, 0, 81, 2, 0, 11, 19, 7, 81, 1, 80, 17645, 12, 22, 22, 80, 39, 1299, 5, 6, 18, 1, 80, 33622, 12, 22, 22, 80, 39, 1299, 0, 81, 6, 0, 5, 7, 81, 1, 80, 41805, 12, 22, 22, 80, 39, 1299, 22, 81, 7, 3, 41, 81, 1969, 1, 80, 38323, 11, 22, 22, 80, 39, 1299, 1, 80, 40812, 11, 22, 22, 80, 39, 1299, 0, 81, 4, 0, 45, 81, 1, 80, 38679, 11, 22, 22, 80, 39, 1299]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 15; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff/qualification.json new file mode 100644 index 00000000..aa806f49 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff/qualification.json @@ -0,0 +1,70 @@ +{ + "id": "switch--stringConcealingControlFlowFlattening--57", + "case": "switch", + "profile": "stringConcealingControlFlowFlattening", + "seed": 57, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-cff/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff/source.js new file mode 100644 index 00000000..98a67db9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-cff/source.js @@ -0,0 +1 @@ +function classify(value) { switch (value) { case 1: return "one"; case 2: return "two"; default: return "other"; } } window.TEST_OUTPUT = [classify(1), classify(3)]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-closures/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-closures/encoded.js new file mode 100644 index 00000000..cd80e9b4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-closures/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"rBjReGOLJID5p3+s8ZjtBlSQUR/dE9paQjgCi5R6E8yyRcK0A6W/y6UajI0/4KrHuKIgrzEFn81d/2U5NgjDW1EkyQL0UmWYqTRiUquRd6obhcIa4ENQM1Hl61YHwBNJ8FLV3fMHMqWfdRxJm4Ja46vukm+Kz5o29AlTLboI4K2WkW0nqvR9SO47DvjuVcC05pCDaIKb1ABnlDdTfE2g+Vk17OopW7YZSAxO9q8Mg2Msem20cl29p6Xk2CKeyNSz8MQTAPYmDRcuckQuC8eTUaEP126euu4ofcRxGD86iq6f44xFDQ14JV5B5OXguSH5pyo9A9LUuEtJtkH6JTo7UVV8dvQYDMyLAUmWNvmcibTFaI7rFijYsWdYjFZ2Ler6u/W5dnqsQKCrdHWzltauLeiKVeowmWCsU6m3J4RG34G1pEcvwBAHR9pQTFbAn1baXiyFh2afkgEa/YT2Z1bpHqOsmqN4JHHPdopF5kD2DBlqT4uN9p7sLSPoPiKce3TNKZ2npqlw3r2sya4s/Wjh8MyLNge6uDYRv5kDYYDoGFPht1XyPrDU0CSO8OvMJMhaRpDwFZ1X9X6A3MUYKRN4e4Z038QfDoo6C7XCt+6jgK/Ru+chIruFPYK6HwabKCEixpv26o7+rbz5CTm0h9pUfHVgd6lGeeLNcAOk9wCvsDsKFr7tqi27xdiJeHPLkuvkcIDoXTYlzJUY2qlxQZTeylKzf8En2Ntk0XC3GAsGHlGIk7sTByv8H9jE1Cn9S+KP4ePORMXq6++GxiGmmY6vUkyEzi15w9DdKz29z3k2hRFFZ70C/sLZTBNshTWziVFr", /* 1 */3, /* 2 */"window", /* 3 */undefined, /* 4 */1, /* 5 */"atob", /* 6 */"", /* 7 */0, /* 8 */"length", /* 9 */"String", /* 10 */"fromCharCode", /* 11 */"charCodeAt", /* 12 */8, /* 13 */2, /* 14 */2654435769, /* 15 */13, /* 16 */65535]; +var BYTECODE = [47, 6, 129, 1, 12, 0, 0, 0, 7, 0, 0, 42, 5, 6, 1, 7, 47, 8, 266, 4, 14, 0, 0, 4, 1, 47, 12, 91, 1, 5, 0, 0, 5, 2, 12, 0, 12, 1, 0, 42, 13, 2, 1, 12, 5, 3, 13, 2, 12, 2, 0, 1, 9, 883262613, 1, 10, 198, 1, 11, 11, 42, 4, 8, 4, 5, 9, 10, 11, 42, 13, 3, 0, 42, 14, 3, 0, 48, 15, 2, 13, 14, 9, 12, 4, 15, 0, 12, 3, 0, 45, 12, 4, 2, 5, 3, 0, 47, 4, 113, 0, 3, 1, 0, 1, 3, 45, 4, 0, 4, 3, 0, 45, 4, 3, 0, 0, 0, 1, 4, 0, 11, 2, 0, 1, 7, 0, 2, 45, 2, 4, 2, 2, 6, 5, 0, 42, 7, 6, 1, 0, 5, 3, 7, 0, 6, 6, 0, 5, 4, 6, 0, 6, 7, 0, 5, 5, 6, 0, 6, 8, 0, 8, 7, 3, 6, 22, 6, 5, 7, 40, 6, 258, 2, 6, 9, 0, 0, 7, 10, 0, 8, 8, 6, 7, 0, 7, 11, 0, 8, 9, 3, 7, 43, 7, 3, 9, 1, 5, 0, 9, 11, 0, 8, 10, 3, 9, 0, 9, 4, 0, 11, 11, 5, 9, 43, 9, 3, 10, 1, 11, 0, 10, 12, 0, 19, 11, 9, 10, 17, 9, 7, 11, 43, 7, 6, 8, 1, 9, 11, 6, 4, 7, 5, 4, 6, 0, 6, 13, 0, 11, 7, 5, 6, 5, 5, 7, 39, 157, 45, 4, 0, 6, 3, 0, 45, 6, 4, 5, 0, 9, 6, 0, 5, 6, 9, 0, 9, 7, 0, 5, 7, 9, 22, 9, 7, 3, 40, 9, 394, 0, 9, 14, 0, 11, 10, 1, 9, 0, 9, 7, 0, 17, 11, 10, 9, 5, 1, 11, 0, 9, 15, 0, 21, 10, 1, 9, 18, 9, 1, 10, 0, 10, 16, 0, 16, 11, 9, 10, 5, 8, 11, 2, 9, 9, 0, 0, 10, 10, 0, 8, 11, 9, 10, 0, 10, 11, 0, 8, 12, 0, 10, 11, 10, 2, 7, 43, 13, 0, 12, 1, 10, 18, 10, 13, 8, 43, 12, 9, 11, 1, 10, 11, 9, 6, 12, 5, 6, 9, 5, 9, 7, 0, 9, 4, 0, 11, 10, 7, 9, 5, 7, 10, 39, 282, 45, 6, 0, 9, 3, 0, 45, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 16; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-closures/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-closures/qualification.json new file mode 100644 index 00000000..c8e2134b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-closures/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "closures--stringConcealing--47", + "case": "closures", + "profile": "stringConcealing", + "seed": 47, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-closures/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-closures/source.js new file mode 100644 index 00000000..8148a2c5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-closures/source.js @@ -0,0 +1 @@ +function counter(start) { let value = start; return () => ++value; } const next = counter(3); window.TEST_OUTPUT = [next(), next()]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/encoded.js new file mode 100644 index 00000000..2773b81d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"uBjbsbA50YD+COHzhxfCoOo48lEnyZJASsuJVwu77iD6mfHx+ogjA+aHAjO+mBRhvel1UfXIO8cLW1bPYnmz4QUpmrA6F4ODIv90kWApdaFOKUfXQHt8z2NbIDFJqUNhUOdPcyWfcAMCiScRdWlC4Rtbmy8Pu4tGRslUcQvpD6NLPzuTphYmgYEJaJGBOnOvhdsHd5TKjUEWya4yI1+z4z1nvxINiZcB5prNDoz77WfIqtE+yUk0wat+49PeV7ai6y79ke352Y7TGtq3NSrCngdq0PHcXuXC9MfYU4ruE2KMmSMulboHBqOLE/76aikmV77/UhvWo0MkzpBSBjbZoRIaFfZpClWefwpftlkaI2J85jZSO85jwkRGONJfegvmR6pePmaKpQZJ+r0NVZbTIkCuQ9IUdk3Cgl0qtqYqFV6V63V29BpyjoUF3JLmbqrjYmaF0l4+jkU+ypt+rOvrJ5C62S7/ldcF4Y7GEpa3HCLF3vl19PXDXpDKjbfFW/bu7HqhlTANoYIiB+2T6v7AYuslwL2ySgHHtntLz/daOCWqrTQdv/ZYAwefoDIntZUdLW259jFbjc9pO1tVdu1wfUjlX5NeP0iD0AZs/TVtcaVIO2avDttSR1DNU10Ytb4kL5+P8wl3dh17jUM1fJV2b537DFf+zCk9rEUn1ed8I+P5J4u89QyShS4F3owuG46nGyuK3Mx1w8XWXOXMwLfUI6Hss3TRleIM6Lz7F/9r2vz/ZP7V3r3kTFfEwHM17I2kGiTRrd8c9oT3Cwac7jQ9pJvdFGzz9C1Epc97NA9UaOw4fIcUO5SUPyiEihQq/K9tZ6RZNEZMccQ7RGTMOVwwpLckO5x19GaEfhxpjG40eaR7bHH8JVSqzCi8h1Uv1BR8RuxdFHG8YAyqhX30hWxLHImncCzw3BdEjcXnXBAsm6d4HKrsMHSJhVMMtLzNF+Rr+fydVNLV6b30TDjU4HP57M6kvTXtjYkczoTYC9+f8SQDpajdFU2uxGpD7c/kNJOk7+0pfeAUGYuwPwOLohRy/YttLNVHM3FPCcOrdAztu10IpYc0E594CxeHDB17rRwlRKVVb0vzO0dhzVa9dVU85Vx/Set3F3S7Xw3dlRD1jW5BI5e3RjMc3RFFVPWnXgcrmKcuG6SOBAWYhRUN+bIPJ8hj3f4vVazFLT2YSgrXnXsa75CamzXNjfgC2vbkc6Wf2iI3teXd4ErQxtBbxK/wOtKm5O3VfdYGLYKSPyOSkWaQ/Zxt+NbBOqpPa9qBdiTqsh1xto4yCp6MAwZ2ehKqrSEmj9oATqP7F0asylG+HyYQ4mV+VJJSBkSCTQ5jkh36cG502mG2ZEoR3lNKYfJRXmYi2bc5EqCOWAKRmXMuZbpPJl5qvP47SvDBFD7yUgLmhmMi7vuSyTnPgbEaa/axay3eyioatpPZ2kLx9vRT2q/1IseWzRHpeZcG3YvzP/aqxGa8+dZR/sb0QvZPDNOnZi/h1BkjsbMKx5/YC/F23BqgoSQ5uNI9T7/DNVe80jK5+SFJ6UV//os+B726LBGdmVThqW5600xHC0Ny2TtxROlzYT0rxbdOG3OBW3lmkUApWaN411tTL/8OQbzJUzHPqCvnSHtND3SZBgEniakQBuelYxffgzAgSe7BGkmP8O5b3a+gODyRuwkggdEIB5ORP+Gg5GiW8d9J0MDUuNMvutuWYOTp8hH7iLsA5p/W8/xo5+CsqQMwj9g3QJH7L1DpKN2h8yn4kDN47oMRCIywPyiHYWn4qnB82AFAb0g80J9pEODQmB8wr4cGAHSIAnBZmFsgCrhX0FlIb8AxSDDQRDhToCoYa3BUCHqARDgqsCUYBeCyaw7QjUgLQBrYQHBn+FtgZ6jtMByYsQEUeKsQcqijP3+oy1CG+RpAkthfsIgvHeCqeCvwthn+gPcIxZ/I69dg/dj5sSw47sDXT+XzyljTII+5+xHaiM8AqosRF4S4FSGGWW3x5kjYw+hHrrAH2eZhLOmdkCsL8YduG6Lwc3l64XYpP7OJ1yxD1vgIUaepKyFJq2oXD3txD3KZfzF5qUNhFOdJc0PfbUFfSUTRU2le53Ebaq/eOg+BsQkMcbcWcKN/P3vTW0nhwRlJjtY7u6kvGJurdm2JoAHdhuYy4p8Q45BmHdG3SQ9BjNr5T577kmb0KoKx8wn9gtl+wpMHF/WiyynYUfr6/06D28Y3/aqu3rFpEvLTniMC5IfLEvLu8WH02aIuPLr4x3dKlv4QaqqhHT6bkmQWSIKmDiqShTkZrs5aAPaPSy/ea0oNNkreV2Iu5i4SQ45cAjKGWw58ek3mU6prPkLKU0Z7+jlSgNYFotgvXJK0dkuCZxpBth4q+p8R6qB2I9qITRBGiNImbo3j32bmEoq9KAbZigp+n+sBJoy64y3vVYzC907e06c3oaLqHqh15AqwntYLzLezGsru32X41fNOBEOuxwxTiP4hZbelnT7FivMHHnukDmOVpiUSrYtSMPeMQx3fVrV6NWbdhmov95kbE4+HOg6Vse1XfUznQVNeP2bDSUVJ/VhthyZKu2gvZptKBVKNcR19tSAjfl8D8890CN2LTS61d9tYb3D7dJccDe29KwX9lAp/quM9J5a8B834Rd/E+UyF2zAnjKwBHZF1KQWonPszhLfFHODs33Xy1bNMyjzM1+hst/yuZf2V+zz6jJkH6HOt7ASkyyUNrMxcBMTdS+fc+rTqJTucu2wK9KcDdIy4NDWUg+1hfFbUYlRnPJbEAVS8/TJssiQItGgMcoRwBHSMSRx8pC0kelxO9ERERdx8THq0SiRfbET0dZR4DJO9NRSblBp8nOMaVBm8TswBRIz0DWyr2yAkhawNHKGFZAT8nBws06eGXDrsjHU9xItMADzT1wts0vz8FYaV8DzsjMIX/XPr7DukxzW6zPRcpseHS/Pc9qTMJfOd3AxVh4EDA4+8NAClne3bfIPUtUswv6zUBFXI/QJt9xQIs48PdYMJdVTtJB3wpAQrll8Ky5dETt03LVmlayNEb0LzT4c7DWu9EhVI60p/Q+tjV229Vc1bVU/1L2+hoyg3lLIuHeiFJ/Vgn1UrZKeyWnTN+EU0xbBOaTOQpzpj4f7sFdyF2z4ei5QXJHuX7l9VrzXRztNC3/fHc8Le4qLQNe6d3gq0h6Ubrq7ROuelye3Gct7G9UMovq3SNmaF/Vxqrhb9u+QPy5rVdpDtDyK3tgcy718Ywot2adKELUqmHBoZjjT6i4YGCrW9CSaA4iB+TdJWRgLCSM5zVi76bG5Hmj02", /* 1 */"1PfWE8SuzSKzWSTR", /* 2 */1162964697, /* 3 */-1199631090, /* 4 */undefined, /* 5 */-1841898343, /* 6 */"EweFixs/iao=", /* 7 */"", /* 8 */1517615109, /* 9 */"fzJyVlziWnpcFkOK", /* 10 */"s4oXHYyFADW7njlj", /* 11 */"SmSj1D48oU04xK50LOyzWwWkpCzcndAF", /* 12 */"pdwTLKmlJxSRjN/714fTDOi8qCU=", /* 13 */418936653, /* 14 */1819780982, /* 15 */1507899148, /* 16 */-999972527]; +var BYTECODE = [47, 5, 194, 1, 12, 0, 0, 0, 6, 0, 1904208776, 42, 4, 5, 1, 6, 47, 7, 331, 4, 14, 0, 0, 4, 1, 47, 11, 91, 1, 13, 1, 0, 1, 4, 5, 2, 11, 2, 11, 1, 2293712668, 1, 8, 1591416361, 1, 9, 228, 1, 10, 11, 42, 3, 7, 4, 4, 8, 9, 10, 0, 12, 2, 1162964696, 42, 13, 2, 1, 12, 0, 12, 3, 3095336205, 42, 14, 2, 1, 12, 48, 12, 2, 13, 14, 9, 11, 3, 12, 0, 11, 4, 0, 45, 11, 3, 6, 0, 47, 7, 331, 4, 14, 0, 0, 4, 2, 0, 11, 2, 1162964696, 26, 12, 0, 11, 40, 12, 116, 39, 131, 0, 11, 5, 2453068955, 26, 12, 0, 11, 40, 12, 129, 39, 150, 39, 169, 1, 8, 2546855986, 1, 9, 243, 1, 10, 3, 42, 3, 7, 4, 6, 8, 9, 10, 45, 3, 1, 8, 4293738767, 1, 9, 251, 1, 10, 3, 42, 4, 7, 4, 6, 8, 9, 10, 45, 4, 1, 8, 1374531232, 1, 9, 254, 1, 10, 5, 42, 5, 7, 4, 6, 8, 9, 10, 45, 5, 0, 11, 4, 0, 45, 11, 4, 2, 2, 6, 6, 404896774, 42, 7, 6, 1, 0, 5, 3, 7, 0, 6, 7, 186948536, 5, 4, 6, 0, 6, 8, 1517615109, 5, 5, 6, 0, 6, 9, 1755286258, 8, 7, 3, 6, 22, 6, 5, 7, 40, 6, 323, 2, 6, 10, 1101265538, 0, 7, 11, 3058060809, 8, 8, 6, 7, 0, 7, 12, 3938263465, 8, 9, 3, 7, 43, 7, 3, 9, 1, 5, 0, 9, 12, 3938263465, 8, 10, 3, 9, 0, 9, 2, 1162964696, 11, 11, 5, 9, 43, 9, 3, 10, 1, 11, 0, 10, 13, 418936645, 19, 11, 9, 10, 17, 9, 7, 11, 43, 7, 6, 8, 1, 9, 11, 6, 4, 7, 5, 4, 6, 0, 6, 5, 2453068955, 11, 7, 5, 6, 5, 5, 7, 39, 222, 45, 4, 0, 6, 4, 0, 45, 6, 4, 5, 0, 9, 7, 186948536, 5, 6, 9, 0, 9, 8, 1517615109, 5, 7, 9, 22, 9, 7, 3, 40, 9, 459, 0, 9, 14, 4064337615, 11, 10, 1, 9, 0, 9, 8, 1517615109, 17, 11, 10, 9, 5, 1, 11, 0, 9, 15, 1507899137, 21, 10, 1, 9, 18, 9, 1, 10, 0, 10, 16, 3294977710, 16, 11, 9, 10, 5, 8, 11, 2, 9, 10, 1101265538, 0, 10, 11, 3058060809, 8, 11, 9, 10, 0, 10, 12, 3938263465, 8, 12, 0, 10, 11, 10, 2, 7, 43, 13, 0, 12, 1, 10, 18, 10, 13, 8, 43, 12, 9, 11, 1, 10, 11, 9, 6, 12, 5, 6, 9, 5, 9, 7, 0, 9, 2, 1162964696, 11, 10, 7, 9, 5, 7, 10, 39, 347, 45, 6, 0, 9, 4, 0, 45, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 15; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/qualification.json new file mode 100644 index 00000000..c58d20d6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/qualification.json @@ -0,0 +1,70 @@ +{ + "id": "switch--stringConcealingConcealConstants--64", + "case": "switch", + "profile": "stringConcealingConcealConstants", + "seed": 64, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "concealConstants": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/source.js new file mode 100644 index 00000000..98a67db9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-concealed-constants/source.js @@ -0,0 +1 @@ +function classify(value) { switch (value) { case 1: return "one"; case 2: return "two"; default: return "other"; } } window.TEST_OUTPUT = [classify(1), classify(3)]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/encoded.js new file mode 100644 index 00000000..fbbd6fdf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"7jOUI06henf6mkE96oNQf29yC4wNotxdlCxv0YvCvlcJJauu8eEU/WC6dX1TDl9uCrK6NhqhWzLzLSyWOM1FBwMzRZoXcuY0c6WhboBqtzogoXjzszaFFCnj3mii+fDimus6QifzVwjMqkObETFqTcEOQ5hV5K1ygmSg6qIdQz8h7VdgZ7nnbBI7S58NCWQRk2ExW8mPctzVMzqy6FttQnmaTyU2Ey34SbpErI+zV6mL8XuPBlz8U0sTm1Iwx0gzpIgm4d5IwR/UtScFThsic+xczeU3HAPxJ82cr2opL6K8kSStMzq3psQLEX8UqCw3Ua1fBcv7sZGyGrfqcEPlQ/zvooIy5h821/mw2072myts6fTtzhILilWMJyJB6coahj6yS5tS8Wg+5B5a/7S1QVPk3A1JaOXCWv7yraUUGSucGKImL0jPDhSsPZl9rQFBEJ2+jKY67XGke1nZlvCv1caSJOzh4/P0UMyNI26xpYxnijNAqYQ+ason7F9kTHwn9hoBPsTNs4dM0ctzSGXVXpojhL4e3JDVA1kaXqhaWWKyrL5PqDXiMjSvp2Is9bxNCt2RRgVLY99qaxzlkB2IHchg9LDU4k+rSz9/q2wh52VSP1yJtwFV/pJstD0YhZkKF/+9a2a/QhjnKFXVoPrAG+tjH16Jo6u6opJFq4XD2VirkrGMLyqDBVRkdwheW9EKH+UmmigNe4SRgD0eCUHzgRUTldc17fMclHjdMQbpr4EJRAWH/zP0mui89WCUQXH4oujqQhRZ3CLWXLTR9JUV3vU9h5qsn+R/jJWglVPZuRyTc5kAa7x2WJfUL7TZ6abFo5OODfGNYynuJTO+y76C3NED2bMdcG50NTf0sD4lJgaklndQ3seQ03XFaKbVJLmuWBGSqrmNPTWfahiLLRzPQN+YiHZclbjkmiRyrLyhJ4MBJLMMQRTv0H6cL4s=", /* 1 */"window", /* 2 */1, /* 3 */3, /* 4 */undefined, /* 5 */2, /* 6 */"atob", /* 7 */"", /* 8 */0, /* 9 */"length", /* 10 */"String", /* 11 */"fromCharCode", /* 12 */"charCodeAt", /* 13 */8, /* 14 */2654435769, /* 15 */13, /* 16 */65535, /* 17 */700204422, /* 18 */"Math", /* 19 */"imul", /* 20 */2419186419, /* 21 */30, /* 22 */1983765468, /* 23 */1256316929, /* 24 */1795702200, /* 25 */16, /* 26 */22, /* 27 */1550410993]; +var BYTECODE = [47, 5, 289, 1, 18, 0, 0, 0, 6, 0, 0, 42, 4, 5, 1, 6, 47, 7, 480, 4, 20, 0, 0, 4, 1, 47, 11, 91, 1, 19, 1, 0, 1, 4, 5, 2, 11, 2, 11, 1, 0, 1, 8, 3138644866, 1, 9, 212, 1, 10, 11, 42, 3, 7, 4, 4, 8, 9, 10, 0, 12, 2, 0, 42, 13, 2, 1, 12, 0, 12, 3, 0, 42, 14, 2, 1, 12, 48, 12, 2, 13, 14, 9, 11, 3, 12, 0, 11, 4, 0, 45, 11, 47, 13, 670, 2, 8, 0, 0, 3, 6, 0, 47, 7, 480, 4, 20, 0, 0, 4, 2, 0, 17, 2, 0, 26, 18, 0, 17, 34, 15, 18, 33, 15, 15, 1, 11, 2791078382, 1, 16, 2706566086, 12, 16, 16, 11, 13, 16, 15, 16, 11, 11, 11, 16, 1, 12, 1791891931, 39, 274, 1, 11, 375985489, 1, 12, 1485188798, 39, 274, 0, 17, 5, 0, 26, 18, 0, 17, 34, 15, 18, 33, 15, 15, 32, 15, 15, 1, 11, 262698388, 1, 16, 916382828, 12, 16, 16, 11, 16, 16, 16, 15, 11, 11, 11, 16, 1, 12, 1450511277, 39, 274, 1, 11, 4090348387, 1, 12, 1468593031, 39, 274, 1, 11, 2524762614, 1, 12, 2489039529, 39, 274, 1, 8, 427924783, 1, 9, 227, 1, 10, 3, 42, 3, 7, 4, 6, 8, 9, 10, 45, 3, 1, 8, 1991122513, 1, 9, 232, 1, 10, 3, 42, 4, 7, 4, 6, 8, 9, 10, 45, 4, 1, 8, 1851056128, 1, 9, 237, 1, 10, 5, 42, 5, 7, 4, 6, 8, 9, 10, 45, 5, 0, 17, 4, 0, 45, 17, 42, 11, 13, 2, 11, 12, 1, 14, 0, 8, 11, 11, 14, 58, 11, 47, 8, 775, 2, 8, 0, 0, 4, 2, 2, 12, 6, 0, 42, 13, 12, 1, 0, 5, 3, 13, 0, 12, 7, 0, 5, 4, 12, 0, 12, 8, 0, 5, 5, 12, 0, 12, 9, 0, 8, 13, 3, 12, 22, 12, 5, 13, 34, 10, 12, 33, 10, 10, 1, 6, 1194619496, 1, 11, 361755396, 12, 11, 11, 6, 13, 11, 10, 11, 11, 6, 6, 11, 1, 7, 1488580960, 39, 465, 2, 12, 10, 0, 0, 13, 11, 0, 8, 14, 12, 13, 0, 13, 12, 0, 8, 15, 3, 13, 43, 13, 3, 15, 1, 5, 0, 15, 12, 0, 8, 16, 3, 15, 0, 15, 2, 0, 11, 17, 5, 15, 43, 15, 3, 16, 1, 17, 0, 16, 13, 0, 19, 17, 15, 16, 17, 15, 13, 17, 43, 13, 12, 14, 1, 15, 11, 12, 4, 13, 5, 4, 12, 0, 12, 5, 0, 11, 13, 5, 12, 5, 5, 13, 1, 6, 1103640077, 1, 7, 1953681172, 39, 465, 45, 4, 0, 12, 4, 0, 45, 12, 42, 6, 8, 2, 6, 7, 1, 9, 0, 8, 6, 6, 9, 58, 6, 47, 12, 899, 3, 10, 0, 0, 4, 5, 0, 15, 7, 0, 5, 6, 15, 0, 15, 8, 0, 5, 7, 15, 22, 15, 7, 3, 34, 13, 15, 33, 13, 13, 1, 9, 4188238330, 1, 14, 2712577004, 12, 14, 14, 9, 13, 14, 13, 14, 11, 9, 9, 14, 1, 10, 41400, 1, 11, 18194, 39, 661, 0, 15, 14, 0, 11, 16, 1, 15, 0, 15, 8, 0, 17, 17, 16, 15, 5, 1, 17, 0, 15, 15, 0, 21, 16, 1, 15, 18, 15, 1, 16, 0, 16, 16, 0, 16, 17, 15, 16, 5, 8, 17, 2, 15, 10, 0, 0, 16, 11, 0, 8, 17, 15, 16, 0, 16, 12, 0, 8, 18, 0, 16, 11, 16, 2, 7, 43, 19, 0, 18, 1, 16, 18, 16, 19, 8, 43, 18, 15, 17, 1, 16, 11, 15, 6, 18, 5, 6, 15, 5, 15, 7, 0, 15, 2, 0, 11, 16, 7, 15, 5, 7, 16, 1, 9, 2477461820, 1, 10, 10330, 1, 11, 13976, 39, 661, 45, 6, 0, 15, 4, 0, 45, 15, 42, 9, 12, 3, 9, 10, 11, 58, 9, 4, 3, 0, 4, 17, 0, 18, 5, 0, 4, 0, 4, 8, 0, 21, 6, 5, 4, 5, 0, 6, 2, 4, 18, 0, 0, 5, 19, 0, 8, 6, 4, 5, 0, 5, 20, 0, 43, 7, 4, 6, 2, 0, 5, 0, 4, 8, 0, 21, 5, 7, 4, 5, 0, 5, 11, 4, 0, 1, 0, 5, 8, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 21, 0, 21, 5, 0, 4, 18, 4, 0, 5, 0, 5, 8, 0, 21, 6, 4, 5, 5, 0, 6, 48, 4, 1, 0, 45, 4, 0, 4, 4, 0, 45, 4, 4, 3, 0, 4, 22, 0, 11, 5, 0, 4, 0, 4, 8, 0, 21, 6, 5, 4, 5, 0, 6, 2, 4, 18, 0, 0, 5, 19, 0, 8, 6, 4, 5, 0, 5, 23, 0, 43, 7, 4, 6, 2, 0, 5, 0, 4, 8, 0, 21, 5, 7, 4, 5, 0, 5, 18, 4, 0, 1, 0, 5, 8, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 21, 0, 21, 5, 0, 4, 18, 4, 0, 5, 0, 5, 8, 0, 21, 6, 4, 5, 5, 0, 6, 0, 4, 24, 0, 12, 5, 0, 4, 0, 4, 8, 0, 21, 6, 5, 4, 5, 0, 6, 48, 4, 1, 0, 45, 4, 0, 4, 4, 0, 45, 4, 4, 4, 0, 6, 25, 0, 19, 7, 2, 6, 17, 6, 1, 7, 0, 7, 8, 0, 21, 8, 6, 7, 5, 5, 8, 0, 6, 26, 0, 21, 7, 0, 6, 18, 6, 0, 7, 0, 7, 8, 0, 21, 8, 6, 7, 5, 0, 8, 2, 6, 18, 0, 0, 7, 19, 0, 8, 8, 6, 7, 0, 7, 27, 0, 43, 9, 6, 8, 2, 0, 7, 0, 6, 8, 0, 21, 7, 9, 6, 5, 0, 7, 18, 6, 0, 5, 0, 7, 8, 0, 21, 8, 6, 7, 5, 0, 8, 35, 6, 0, 0, 7, 8, 0, 21, 8, 6, 7, 5, 0, 8, 45, 0, 0, 6, 4, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 15; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/qualification.json new file mode 100644 index 00000000..476a92ae --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/qualification.json @@ -0,0 +1,70 @@ +{ + "id": "switch--stringConcealingDispatcher--57", + "case": "switch", + "profile": "stringConcealingDispatcher", + "seed": 57, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "dispatcher": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/source.js new file mode 100644 index 00000000..98a67db9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-dispatcher/source.js @@ -0,0 +1 @@ +function classify(value) { switch (value) { case 1: return "one"; case 2: return "two"; default: return "other"; } } window.TEST_OUTPUT = [classify(1), classify(3)]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/encoded.js new file mode 100644 index 00000000..c4656a20 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"rBjReGOLJID5p3+s8ZjtBlSQUR/dE9paQjgCi5R6E8yyRcK0A6W/y6UajI0/4KrHuKIgrzEFn81d/2U5NgjDW1EkyQL0UmWYqTRiUquRd6obhcIa4ENQM1Hl61YHwBNJ8FLV3fMHMqWfdRxJm4Ja46vukm+Kz5o29AlTLboI4K2WkW0nqvR9SO47DvjuVcC05pCDaIKb1ABnlDdTfE2g+Vk17OopW7YZSAxO9q8Mg2Msem20cl29p6Xk2CKeyNSz8MQTAPYmDRcuckQuC8eTUaEP126euu4ofcRxGD86iq6f44xFDQ14JV5B5OXguSH5pyo9A9LUuEtJtkH6JTo7UVV8dvQYDMyLAUmWNvmcibTFaI7rFijYsWdYjFZ2Ler6u/W5dnqsQKCrdHWzltauLeiKVeowmWCsU6m3J4RG34G1pEcvwBAHR9pQTFbAn1baXiyFh2afkgEa/YT2Z1bpHqOsmqN4JHHPdopF5kD2DBlqT4uN9p7sLSPoPiKce3TNKZ2npqlw3r2sya4s/Wjh8MyLNge6uDYRv5kDYYDoGFPht9iLwv/GZPMknr0ITYbECkPU0CSO8OvMJHmXFWGa+RdRf8t2P3RLZNfwFZ1X9X54+5VvDJt4MWmJegFniSkTeHuGdN/EHw6KOgu1wrfuo4Cv0bvnISK7hT2Cuh8GmyghIsab9uqO/q28+Qk5tIfaVHx1YHepRnnizXADpPcAr7A7Cha+7aotu8XYiXhzy5Lr5HCA6F02JcyVGNqpcUGU3spSs3/BJ9jbZNFwtxgLBh5RiJO7Ewcr/B/YxNQp/Uvij+HjzkTF6uvvhsYhppmOr1JMhM4tecPQ3Ss9vc95NoURRWe9Av7C2UwTbIU1s4lRazdsdBo20IZTqwPsbmQtJTOq0bAkiy+f2TMBqT9o9NX8ehqE7bXEBua5EkzwtR6eBg==", /* 1 */"window", /* 2 */1, /* 3 */4, /* 4 */undefined, /* 5 */0, /* 6 */"Error", /* 7 */true, /* 8 */"atob", /* 9 */"", /* 10 */"length", /* 11 */"String", /* 12 */"fromCharCode", /* 13 */"charCodeAt", /* 14 */8, /* 15 */2, /* 16 */2654435769, /* 17 */13, /* 18 */65535]; +var BYTECODE = [47, 6, 279, 1, 12, 0, 0, 0, 7, 0, 0, 42, 5, 6, 1, 7, 47, 8, 416, 4, 14, 0, 0, 4, 1, 47, 12, 120, 1, 16, 1, 0, 1, 5, 5, 2, 12, 2, 12, 1, 0, 1, 9, 883262613, 1, 10, 198, 1, 11, 11, 42, 3, 8, 4, 5, 9, 10, 11, 0, 13, 2, 0, 32, 14, 13, 42, 13, 2, 1, 14, 0, 14, 3, 0, 42, 15, 2, 1, 14, 2, 14, 1, 0, 1, 9, 4065695261, 1, 10, 209, 1, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 8, 16, 14, 4, 48, 14, 3, 13, 15, 16, 9, 12, 3, 14, 0, 12, 4, 0, 45, 12, 3, 9, 0, 47, 10, 416, 4, 14, 0, 0, 4, 2, 59, 236, 4, 5, 267, 54, 200, 3, 0, 14, 5, 0, 22, 15, 0, 14, 40, 15, 179, 2, 14, 6, 0, 1, 11, 1523115356, 1, 12, 221, 1, 13, 8, 42, 6, 10, 4, 9, 11, 12, 13, 44, 15, 14, 1, 6, 46, 15, 0, 14, 2, 0, 11, 15, 0, 14, 55, 5, 5, 15, 1, 4, 269, 55, 39, 236, 55, 39, 230, 1, 11, 3699422491, 1, 12, 232, 1, 13, 7, 42, 7, 10, 4, 9, 11, 12, 13, 8, 14, 3, 7, 5, 5, 14, 1, 4, 271, 55, 39, 236, 55, 1, 4, 273, 39, 236, 2, 14, 1, 0, 1, 11, 4065695261, 1, 12, 209, 1, 13, 8, 42, 8, 10, 4, 9, 11, 12, 13, 0, 15, 7, 0, 9, 14, 8, 15, 58, 4, 46, 5, 45, 5, 45, 5, 0, 14, 4, 0, 45, 14, 4, 2, 2, 6, 8, 0, 42, 7, 6, 1, 0, 5, 3, 7, 0, 6, 9, 0, 5, 4, 6, 0, 6, 5, 0, 5, 5, 6, 0, 6, 10, 0, 8, 7, 3, 6, 22, 6, 5, 7, 40, 6, 408, 2, 6, 11, 0, 0, 7, 12, 0, 8, 8, 6, 7, 0, 7, 13, 0, 8, 9, 3, 7, 43, 7, 3, 9, 1, 5, 0, 9, 13, 0, 8, 10, 3, 9, 0, 9, 2, 0, 11, 11, 5, 9, 43, 9, 3, 10, 1, 11, 0, 10, 14, 0, 19, 11, 9, 10, 17, 9, 7, 11, 43, 7, 6, 8, 1, 9, 11, 6, 4, 7, 5, 4, 6, 0, 6, 15, 0, 11, 7, 5, 6, 5, 5, 7, 39, 307, 45, 4, 0, 6, 4, 0, 45, 6, 4, 5, 0, 9, 9, 0, 5, 6, 9, 0, 9, 5, 0, 5, 7, 9, 22, 9, 7, 3, 40, 9, 544, 0, 9, 16, 0, 11, 10, 1, 9, 0, 9, 5, 0, 17, 11, 10, 9, 5, 1, 11, 0, 9, 17, 0, 21, 10, 1, 9, 18, 9, 1, 10, 0, 10, 18, 0, 16, 11, 9, 10, 5, 8, 11, 2, 9, 11, 0, 0, 10, 12, 0, 8, 11, 9, 10, 0, 10, 13, 0, 8, 12, 0, 10, 11, 10, 2, 7, 43, 13, 0, 12, 1, 10, 18, 10, 13, 8, 43, 12, 9, 11, 1, 10, 11, 9, 6, 12, 5, 6, 9, 5, 9, 7, 0, 9, 2, 0, 11, 10, 7, 9, 5, 7, 10, 39, 432, 45, 6, 0, 9, 4, 0, 45, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 17; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/qualification.json new file mode 100644 index 00000000..bcd926d7 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/qualification.json @@ -0,0 +1,69 @@ +{ + "id": "exceptions--stringConcealing--47", + "case": "exceptions", + "profile": "stringConcealing", + "seed": 47, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-methods/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-methods/encoded.js new file mode 100644 index 00000000..fc0c787f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-methods/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"rBjReGOLJID5p3+s8ZjtBlSQUR/dE9paQjgCi5R6E8yyRcK0A6W/y6UajI0/4KrHuKIgrzEFn81d/2U5NgjDW1EkyQL0UmWYqTRiUquRd6obhcIa4ENQM1Hl61YHwBNJ8FLV3fMHMqWfdRxJm4Ja46vukm+Kz5o29AlTLboI4K2WkW0nqvR9SO47DvjuVcC05pCDaIKb1ABnlDdTfE2g+Vk17OopW7YZSAxO9q8Mg2Msem20cl29p6Xk2CKeyNSz8MQTAPYmDRcuckQuC8eTUaEP126euu4ofcRxGD86iq6f44xFDQ14JV5B5OXguSH5pyo9A9LUuEtJtkH6JTo7UVV8dvQYDMyLAUmWNvmcibTFaI7rFijYsWdYjFZ2Ler6u/W5dnqsQKCrdHWzltauLeiKVeowmWCsU6m3J4RG34G1pEcvwBAHR9pQTFbAn1baXiyFh2afkgEa/YT2Z1bpHqOsmqN4JHHPdopF5kD2DBlqT4uN9p7sLSPoPiKce3TNKZ2npqlw3r2sya4s32jF8POLFweAuNqLxP/dZPgkgb0FTdTQJI7w68wkQ5c1Ya75IlFUy1A/V0tV12x5XuFCGfAVnVf1foDcxRgpE3h7hnTfxB8OijoLtcK37qOAr9G75yEiu4U9grofBpsoISLGm/bqjv6tvPkJObSH2lR8dWB3qUZ54s1wA6T3AK+wOwoWvu2qLbvF2Il4c8uS6+RwgOhdNiXMlRjaqXFBlN7KUrN/wSfY22TRcLcYCwYeUYiTuxMHK/wf2MTUKf1L4o/h485Exerr74bGIaaZjq9STITOLXnD0N0rPb3PeTaFEUVnvQL+wtlME2yFNbOJUWs3bHQaNtCGU6sD7G5kLSUzqtGwJIsvn9kzAak/aPTV/HoahO21xAbmuRJM8LUengZPSvluI+77EehuuBkTGPOpQAbmomCyAxFqz1kzu8ikFiqntosgHyCuZoE7UYxMtK2fwqKzFH86WH8NSeklRwBCeqRsj0idKRSBcT0+9Y3K5Y/pDtKDbigZp51EfqtpkUuG+zVSMsDc1QI9S1AFIgEKelNMMqRbtxxDICzz8eU8aSAJkgI=", /* 1 */3, /* 2 */"window", /* 3 */undefined, /* 4 */2, /* 5 */"atob", /* 6 */"", /* 7 */0, /* 8 */"length", /* 9 */"String", /* 10 */"fromCharCode", /* 11 */"charCodeAt", /* 12 */1, /* 13 */8, /* 14 */2654435769, /* 15 */13, /* 16 */65535]; +var BYTECODE = [47, 8, 192, 1, 12, 0, 0, 0, 9, 0, 0, 42, 7, 8, 1, 9, 47, 10, 329, 4, 14, 0, 0, 4, 1, 49, 14, 0, 1, 11, 883262613, 1, 12, 198, 1, 13, 5, 42, 3, 10, 4, 7, 11, 12, 13, 0, 15, 1, 0, 9, 14, 3, 15, 1, 11, 4065695261, 1, 12, 203, 1, 13, 6, 42, 4, 10, 4, 7, 11, 12, 13, 47, 15, 143, 0, 11, 1, 0, 1, 7, 9, 14, 4, 15, 5, 2, 14, 2, 14, 2, 0, 1, 11, 1523115356, 1, 12, 213, 1, 13, 11, 42, 5, 10, 4, 7, 11, 12, 13, 1, 11, 4065695261, 1, 12, 203, 1, 13, 6, 42, 6, 10, 4, 7, 11, 12, 13, 8, 15, 2, 6, 43, 16, 2, 15, 0, 9, 14, 5, 16, 0, 14, 3, 0, 45, 14, 3, 3, 0, 47, 4, 329, 4, 14, 0, 0, 4, 1, 1, 5, 883262613, 1, 6, 198, 1, 7, 5, 42, 2, 4, 4, 3, 5, 6, 7, 8, 8, 1, 2, 0, 9, 4, 0, 13, 10, 8, 9, 45, 10, 0, 8, 3, 0, 45, 8, 4, 2, 2, 6, 5, 0, 42, 7, 6, 1, 0, 5, 3, 7, 0, 6, 6, 0, 5, 4, 6, 0, 6, 7, 0, 5, 5, 6, 0, 6, 8, 0, 8, 7, 3, 6, 22, 6, 5, 7, 40, 6, 321, 2, 6, 9, 0, 0, 7, 10, 0, 8, 8, 6, 7, 0, 7, 11, 0, 8, 9, 3, 7, 43, 7, 3, 9, 1, 5, 0, 9, 11, 0, 8, 10, 3, 9, 0, 9, 12, 0, 11, 11, 5, 9, 43, 9, 3, 10, 1, 11, 0, 10, 13, 0, 19, 11, 9, 10, 17, 9, 7, 11, 43, 7, 6, 8, 1, 9, 11, 6, 4, 7, 5, 4, 6, 0, 6, 4, 0, 11, 7, 5, 6, 5, 5, 7, 39, 220, 45, 4, 0, 6, 3, 0, 45, 6, 4, 5, 0, 9, 6, 0, 5, 6, 9, 0, 9, 7, 0, 5, 7, 9, 22, 9, 7, 3, 40, 9, 457, 0, 9, 14, 0, 11, 10, 1, 9, 0, 9, 7, 0, 17, 11, 10, 9, 5, 1, 11, 0, 9, 15, 0, 21, 10, 1, 9, 18, 9, 1, 10, 0, 10, 16, 0, 16, 11, 9, 10, 5, 8, 11, 2, 9, 9, 0, 0, 10, 10, 0, 8, 11, 9, 10, 0, 10, 11, 0, 8, 12, 0, 10, 11, 10, 2, 7, 43, 13, 0, 12, 1, 10, 18, 10, 13, 8, 43, 12, 9, 11, 1, 10, 11, 9, 6, 12, 5, 6, 9, 5, 9, 7, 0, 9, 12, 0, 11, 10, 7, 9, 5, 7, 10, 39, 345, 45, 6, 0, 9, 3, 0, 45, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 17; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-methods/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-methods/qualification.json new file mode 100644 index 00000000..f1ef77c8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-methods/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "methods--stringConcealing--47", + "case": "methods", + "profile": "stringConcealing", + "seed": 47, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 6 + }, + "encodedObservation": { + "kind": "value", + "value": 6 + }, + "decodedObservation": { + "kind": "value", + "value": 6 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-methods/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-methods/source.js new file mode 100644 index 00000000..f7e1e131 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-methods/source.js @@ -0,0 +1 @@ +const object = { value: 3, double() { return this.value * 2; } }; window.TEST_OUTPUT = object.double(); diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/encoded.js new file mode 100644 index 00000000..1f9353bf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"7jOUI06henf6mkE96oNQf29yC4wNotxdlCxv0YvCvlcJJauu8eEU/WC6dX1TDl9uCrK6NhqhWzLzLSyWOM1FBwMzRZoXcuY0c6WhboBqtzogoXjzszaFFCnj3mii+fDimus6QifzVwjMqkObETFqTcEOQ5hV5K1ygmSg6qIdQz8h7VdgZ7nnbBI7S58NCWQRk2ExW8mPctzVMzqy6FttQnmaTyU2Ey34SbpErI+zV6mL8XuPBlz8U0sTm1Iwx0gzpIgm4d5IwR/UtScFThsic+xczeU3HAPxJ82cr2opL6K8kSStMzq3psQLEX8UqCw3Ua1fBcv7sZGyGrfqcEPlQ/zvooIy5h821/mw2072myts6fTtzhILilWMJyJB6coahj6yS5tS8Wg+5B5a/7S1QVPk3A1JaOXCWv7yraUUGSucGKImL0jPDhSsPZl9rQFBEJ2+jKY67XGke1nZlvCv1caSJOzh4/P0UMyNI26xpYxnijNAqYQ+ason7F9kTHwn9hoBPsTNs4dM0ctzSGXVXpojhL4e3JDVA1kaXqhaWWKyrL5PqDXiMjSvp2Is9bxNCt2RRgVLY99qaxzlkB2IHchg9LDU4kmrQT9sIedlHD9V/pJsVG5CHGa/QhjnKFXVoPrAG+tjH16Jo6u6opJFq4XD2VirkrGMLyqDBVRkdwheW9EKH+UmmigNe4SRgD0eCUHzgRUTldc17fMclHjdMQbpr4EJRAWH/zP0mui89WCUQXH4oujqQhRZ3CLWXLTR9JUV3vU9h5qsn+R/jJWglVPZuRyTc5kAa7x2WJfUL7TZ6abFo5OODfGNYynuJTO+y76C3NED2bMdcG50NTf0sD4lJgaklndQ3seQ03XFaKbVJLmuWBGSqrmNPTWfahiLLRzPQN+YiHZclbjkmiRyrLyhJ4MBJLMMQRTv0H6cL4sxkOzmlotYHQMjvCkEQrnfHzzXCxSaVxJwgkB+zlCk+h8rzmrt8U0vhlh1VtlrJfqIV1uU4UoP6Rh/d3nNdwhrzzo9diO3xklZyg==", /* 1 */"window", /* 2 */7, /* 3 */undefined, /* 4 */32651, /* 5 */0, /* 6 */23657, /* 7 */38771, /* 8 */8961, /* 9 */19946, /* 10 */8098, /* 11 */18649, /* 12 */14058, /* 13 */21865, /* 14 */19431, /* 15 */31648, /* 16 */43776, /* 17 */7543, /* 18 */14523, /* 19 */13892, /* 20 */33617, /* 21 */49882, /* 22 */"String", /* 23 */"fromCharCode", /* 24 */8, /* 25 */"", /* 26 */"charCodeAt", /* 27 */1, /* 28 */"atob", /* 29 */"length", /* 30 */2, /* 31 */21007, /* 32 */38043, /* 33 */27498, /* 34 */17636, /* 35 */28853, /* 36 */9961, /* 37 */2549, /* 38 */2686, /* 39 */35719, /* 40 */34531, /* 41 */41327, /* 42 */1871, /* 43 */26202, /* 44 */22945, /* 45 */19717, /* 46 */11874, /* 47 */10304, /* 48 */7154, /* 49 */65535, /* 50 */2654435769, /* 51 */13]; +var BYTECODE = [47, 6, 198, 1, 69, 0, 0, 0, 7, 0, 0, 42, 5, 6, 1, 7, 47, 8, 784, 4, 77, 0, 0, 4, 1, 47, 12, 103, 1, 4, 1, 0, 1, 5, 5, 2, 12, 2, 12, 1, 0, 1, 9, 3138644866, 1, 10, 212, 1, 11, 11, 42, 3, 8, 4, 5, 9, 10, 11, 1, 9, 427924783, 1, 10, 227, 1, 11, 2, 42, 4, 8, 4, 5, 9, 10, 11, 42, 13, 2, 1, 4, 0, 14, 2, 0, 42, 15, 13, 1, 14, 42, 13, 15, 0, 9, 12, 3, 13, 0, 12, 3, 0, 45, 12, 4, 2, 47, 3, 124, 1, 4, 2, 0, 1, 0, 0, 0, 45, 3, 0, 3, 3, 0, 45, 3, 4, 2, 47, 3, 147, 0, 11, 3, 0, 0, 0, 1, 0, 0, 1, 45, 3, 0, 3, 3, 0, 45, 3, 3, 3, 2, 47, 4, 784, 4, 77, 0, 0, 4, 1, 3, 8, 0, 1, 5, 1991122513, 1, 6, 231, 1, 7, 1, 42, 2, 4, 4, 3, 5, 6, 7, 11, 9, 8, 2, 3, 8, 1, 11, 10, 9, 8, 45, 10, 0, 8, 3, 0, 45, 8, 4, 15, 0, 16, 4, 0, 5, 17, 16, 0, 18, 5, 0, 5, 19, 18, 0, 20, 6, 0, 27, 21, 17, 20, 40, 21, 523, 0, 22, 7, 0, 5, 19, 22, 26, 23, 17, 19, 40, 23, 241, 39, 564, 0, 24, 8, 0, 12, 25, 19, 24, 5, 19, 25, 26, 26, 17, 19, 40, 26, 261, 39, 768, 0, 27, 9, 0, 11, 28, 19, 27, 5, 19, 28, 26, 29, 17, 19, 40, 29, 281, 39, 742, 0, 30, 10, 0, 11, 31, 19, 30, 5, 19, 31, 26, 32, 17, 19, 40, 32, 301, 39, 647, 0, 33, 11, 0, 12, 34, 19, 33, 5, 19, 34, 26, 35, 17, 19, 40, 35, 321, 39, 532, 0, 36, 12, 0, 12, 37, 19, 36, 5, 19, 37, 26, 38, 17, 19, 40, 38, 341, 39, 762, 0, 39, 13, 0, 12, 40, 19, 39, 5, 19, 40, 26, 41, 17, 19, 40, 41, 361, 39, 693, 0, 42, 14, 0, 11, 43, 19, 42, 5, 19, 43, 26, 44, 17, 19, 40, 44, 381, 39, 606, 0, 45, 15, 0, 11, 46, 19, 45, 5, 19, 46, 26, 47, 17, 19, 40, 47, 401, 39, 523, 0, 48, 16, 0, 12, 49, 19, 48, 5, 19, 49, 26, 50, 17, 19, 40, 50, 421, 39, 709, 0, 51, 17, 0, 11, 52, 19, 51, 5, 19, 52, 26, 53, 17, 19, 40, 53, 441, 39, 562, 0, 54, 18, 0, 11, 55, 19, 54, 5, 19, 55, 26, 56, 17, 19, 40, 56, 461, 39, 627, 0, 57, 19, 0, 12, 58, 19, 57, 5, 19, 58, 26, 59, 17, 19, 40, 59, 481, 39, 670, 0, 60, 20, 0, 11, 61, 19, 60, 5, 19, 61, 26, 62, 17, 19, 40, 62, 501, 39, 587, 0, 63, 21, 0, 12, 64, 19, 63, 5, 19, 64, 26, 65, 17, 19, 40, 65, 521, 39, 553, 39, 214, 1, 66, 51867, 12, 17, 17, 66, 39, 214, 2, 7, 22, 0, 0, 67, 23, 0, 8, 8, 7, 67, 1, 66, 18649, 11, 17, 17, 66, 39, 214, 1, 66, 22653, 11, 17, 17, 66, 39, 214, 45, 4, 11, 67, 5, 13, 43, 12, 3, 11, 1, 67, 0, 14, 24, 0, 1, 66, 20012, 12, 17, 17, 66, 39, 214, 5, 3, 6, 0, 67, 25, 0, 5, 4, 67, 1, 66, 49094, 12, 17, 17, 66, 39, 214, 0, 67, 26, 0, 8, 11, 3, 67, 0, 13, 27, 0, 1, 66, 16058, 11, 17, 17, 66, 39, 214, 4, 2, 2, 67, 28, 0, 42, 6, 67, 1, 0, 1, 66, 19725, 11, 17, 17, 66, 39, 214, 0, 67, 26, 0, 8, 68, 3, 67, 43, 10, 3, 68, 1, 5, 1, 66, 35141, 12, 17, 17, 66, 39, 214, 19, 67, 12, 14, 17, 68, 10, 67, 43, 9, 7, 8, 1, 68, 1, 66, 11051, 11, 17, 17, 66, 39, 214, 0, 67, 5, 0, 5, 5, 67, 1, 66, 7303, 11, 17, 17, 66, 39, 214, 0, 67, 29, 0, 8, 68, 3, 67, 22, 67, 5, 68, 41, 67, 733, 1, 66, 7543, 11, 17, 17, 66, 39, 214, 1, 66, 28620, 11, 17, 17, 66, 39, 214, 0, 67, 30, 0, 11, 68, 5, 67, 5, 5, 68, 1, 66, 39171, 12, 17, 17, 66, 39, 214, 0, 67, 3, 0, 45, 67, 11, 67, 4, 9, 5, 4, 67, 1, 66, 19946, 11, 17, 17, 66, 39, 214, 4, 20, 0, 21, 31, 0, 5, 22, 21, 0, 23, 5, 0, 5, 24, 23, 0, 25, 32, 0, 27, 26, 22, 25, 40, 26, 1129, 0, 27, 33, 0, 5, 24, 27, 26, 28, 22, 24, 40, 28, 827, 39, 1304, 0, 29, 34, 0, 11, 30, 24, 29, 5, 24, 30, 26, 31, 22, 24, 40, 31, 847, 39, 1261, 0, 32, 35, 0, 12, 33, 24, 32, 5, 24, 33, 26, 34, 22, 24, 40, 34, 867, 39, 1284, 0, 35, 36, 0, 11, 36, 24, 35, 5, 24, 36, 26, 37, 22, 24, 40, 37, 887, 39, 1313, 0, 38, 37, 0, 12, 39, 24, 38, 5, 24, 39, 26, 40, 22, 24, 40, 40, 907, 39, 1131, 0, 41, 38, 0, 12, 42, 24, 41, 5, 24, 42, 26, 43, 22, 24, 40, 43, 927, 39, 1158, 0, 44, 39, 0, 11, 45, 24, 44, 5, 24, 45, 26, 46, 22, 24, 40, 46, 947, 39, 1176, 0, 47, 40, 0, 12, 48, 24, 47, 5, 24, 48, 26, 49, 22, 24, 40, 49, 967, 39, 1362, 0, 50, 41, 0, 11, 51, 24, 50, 5, 24, 51, 26, 52, 22, 24, 40, 52, 987, 39, 1350, 0, 53, 42, 0, 11, 54, 24, 53, 5, 24, 54, 26, 55, 22, 24, 40, 55, 1007, 39, 1382, 0, 56, 43, 0, 12, 57, 24, 56, 5, 24, 57, 26, 58, 22, 24, 40, 58, 1027, 39, 1129, 0, 59, 44, 0, 12, 60, 24, 59, 5, 24, 60, 26, 61, 22, 24, 40, 61, 1047, 39, 1240, 0, 62, 45, 0, 11, 63, 24, 62, 5, 24, 63, 26, 64, 22, 24, 40, 64, 1067, 39, 1152, 0, 65, 46, 0, 11, 66, 24, 65, 5, 24, 66, 26, 67, 22, 24, 40, 67, 1087, 39, 1197, 0, 68, 47, 0, 11, 69, 24, 68, 5, 24, 69, 26, 70, 22, 24, 40, 70, 1107, 39, 1217, 0, 71, 48, 0, 11, 72, 24, 71, 5, 24, 72, 26, 73, 22, 24, 40, 73, 1127, 39, 1338, 39, 800, 45, 6, 0, 75, 23, 0, 8, 15, 14, 75, 0, 16, 26, 0, 1, 74, 34448, 11, 22, 22, 74, 39, 800, 0, 75, 3, 0, 45, 75, 4, 5, 0, 75, 25, 0, 5, 6, 75, 1, 74, 44386, 11, 22, 22, 74, 39, 800, 21, 75, 1, 11, 18, 12, 1, 75, 0, 13, 49, 0, 1, 74, 8889, 12, 22, 22, 74, 39, 800, 16, 75, 12, 13, 5, 8, 75, 2, 14, 22, 0, 1, 74, 24144, 12, 22, 22, 74, 39, 800, 8, 75, 0, 16, 11, 76, 2, 7, 43, 17, 0, 75, 1, 76, 1, 74, 13007, 12, 22, 22, 74, 39, 800, 0, 75, 50, 0, 11, 9, 1, 75, 0, 10, 5, 0, 1, 74, 35, 11, 22, 22, 74, 39, 800, 18, 75, 17, 8, 43, 76, 14, 15, 1, 75, 11, 18, 6, 76, 1, 74, 18388, 11, 22, 22, 74, 39, 800, 17, 75, 9, 10, 5, 1, 75, 0, 11, 51, 0, 1, 74, 40445, 11, 22, 22, 74, 39, 800, 1, 74, 11693, 11, 22, 22, 74, 39, 800, 22, 75, 7, 3, 41, 75, 1329, 1, 74, 12949, 11, 22, 22, 74, 39, 800, 1, 74, 9996, 12, 22, 22, 74, 39, 800, 5, 7, 19, 1, 74, 39053, 12, 22, 22, 74, 39, 800, 5, 6, 18, 1, 74, 41327, 12, 22, 22, 74, 39, 800, 5, 75, 7, 0, 75, 27, 0, 11, 19, 7, 75, 1, 74, 43100, 11, 22, 22, 74, 39, 800, 0, 75, 5, 0, 5, 7, 75, 1, 74, 39151, 12, 22, 22, 74, 39, 800]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 16; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/qualification.json new file mode 100644 index 00000000..35a029f9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/qualification.json @@ -0,0 +1,61 @@ +{ + "id": "nestedStrings--stringConcealingControlFlowFlattening--57", + "case": "nestedStrings", + "profile": "stringConcealingControlFlowFlattening", + "seed": 57, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": true, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": "id:7" + }, + "encodedObservation": { + "kind": "value", + "value": "id:7" + }, + "decodedObservation": { + "kind": "value", + "value": "id:7" + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/source.js new file mode 100644 index 00000000..fbfe36eb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested-cff/source.js @@ -0,0 +1 @@ +function outer(prefix) { return function middle(n) { return function inner() { return prefix + ":" + n; }; }; } window.TEST_OUTPUT = outer("id")(7)(); diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-nested/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested/encoded.js new file mode 100644 index 00000000..32fc7c8e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"KNK6D0H3JhVdiAATwcWwiSb0rKWZ8Q+8wNQB+2uUYSZv1OMWjYbffi3yZMzkymM5za3iblQjSBJ+owgtwSMh0NNLtM3B26Mb3/NF8B17/hpCOFlBTeMlEWXQJ4x63tjIsRiO9O3oHdre275TDKUYzk6VPX8NCL+hsAc35rhxtuFNMKU0EndRrW0mK0tSNkl9Fwa+sxwxGj6VvKloI2dZSKSWeP/sfeEve83D/h7gAxvFr5h9Q7uTDjgNXSBj6/h/kE0JXuI66DU0Y2pPkgXFFQPynJPzndtZ4k/4hgsh4FOKRS60uywuXLLnH2yGHBqEaASQnBpskWGkbvVDsVo/9zFGYBdE6QpNDWA9A3Vt354WWNuj2lit3AyZ/crmHPwyt0UMkb00+mlWLqB9rk81VpNLQAywy0jfWyWO2jH/tL6cIbHGleSiTqMvnefu3KlUynHxAGSaryFK/bHMaFMabV0s6Bq33Tb921I6I987Hak/AutwsID/t57mHnMLhG8HUzEf76XmASZGReEeWM1Y1rdTIgE33BYw5JJCiRYyiAvADYAYfPGl3UK5MV+iC5YkkDpdl/vL6bxiT0g8/b5mEZpvmywJvdRrJcFJ0apX71Cb8wjciiYuG2YdQFF7WHe85AztXmzrY0PON+KNbq+htzJXsXrwB/sk1JikDswgSLAEx1rjIuZiLNLVLCoK5p4sSNGApxL7O2tCD8TixozsPtUQ/53KYlIOorlLUD/FVb8jTB4C66mepche9hDzqC5/d6UFw+1dmYM2+YbDuCZzKsHZ168mR0/wXKnnGS715GQz9t7tNSn0TsFdSdcZGrEpOV9Ig/zQQfpPNmjZh4Ei0zs1cU0dUJgiyck3jmzj3BlL8BlR6oo2TklIwS7wHoedoME6PkyqvEm8/MoXg49Y4Q74hTZk4WDP9GQgz4SphWOhhxA2", /* 1 */"window", /* 2 */7, /* 3 */undefined, /* 4 */"atob", /* 5 */"", /* 6 */0, /* 7 */"length", /* 8 */"String", /* 9 */"fromCharCode", /* 10 */"charCodeAt", /* 11 */1, /* 12 */8, /* 13 */2, /* 14 */2654435769, /* 15 */13, /* 16 */65535]; +var BYTECODE = [47, 6, 198, 1, 12, 0, 0, 0, 7, 0, 0, 42, 5, 6, 1, 7, 47, 8, 335, 4, 14, 0, 0, 4, 1, 47, 12, 103, 1, 4, 1, 0, 1, 5, 5, 2, 12, 2, 12, 1, 0, 1, 9, 1798083909, 1, 10, 112, 1, 11, 11, 42, 3, 8, 4, 5, 9, 10, 11, 1, 9, 3605756438, 1, 10, 126, 1, 11, 2, 42, 4, 8, 4, 5, 9, 10, 11, 42, 13, 2, 1, 4, 0, 14, 2, 0, 42, 15, 13, 1, 14, 42, 13, 15, 0, 9, 12, 3, 13, 0, 12, 3, 0, 45, 12, 4, 2, 47, 3, 124, 1, 4, 2, 0, 1, 0, 0, 0, 45, 3, 0, 3, 3, 0, 45, 3, 4, 2, 47, 3, 147, 0, 11, 3, 0, 0, 0, 1, 0, 0, 1, 45, 3, 0, 3, 3, 0, 45, 3, 3, 3, 2, 47, 4, 335, 4, 14, 0, 0, 4, 1, 3, 8, 0, 1, 5, 781063577, 1, 6, 130, 1, 7, 1, 42, 2, 4, 4, 3, 5, 6, 7, 11, 9, 8, 2, 3, 8, 1, 11, 10, 9, 8, 45, 10, 0, 8, 3, 0, 45, 8, 4, 2, 2, 6, 4, 0, 42, 7, 6, 1, 0, 5, 3, 7, 0, 6, 5, 0, 5, 4, 6, 0, 6, 6, 0, 5, 5, 6, 0, 6, 7, 0, 8, 7, 3, 6, 22, 6, 5, 7, 40, 6, 327, 2, 6, 8, 0, 0, 7, 9, 0, 8, 8, 6, 7, 0, 7, 10, 0, 8, 9, 3, 7, 43, 7, 3, 9, 1, 5, 0, 9, 10, 0, 8, 10, 3, 9, 0, 9, 11, 0, 11, 11, 5, 9, 43, 9, 3, 10, 1, 11, 0, 10, 12, 0, 19, 11, 9, 10, 17, 9, 7, 11, 43, 7, 6, 8, 1, 9, 11, 6, 4, 7, 5, 4, 6, 0, 6, 13, 0, 11, 7, 5, 6, 5, 5, 7, 39, 226, 45, 4, 0, 6, 3, 0, 45, 6, 4, 5, 0, 9, 5, 0, 5, 6, 9, 0, 9, 6, 0, 5, 7, 9, 22, 9, 7, 3, 40, 9, 463, 0, 9, 14, 0, 11, 10, 1, 9, 0, 9, 6, 0, 17, 11, 10, 9, 5, 1, 11, 0, 9, 15, 0, 21, 10, 1, 9, 18, 9, 1, 10, 0, 10, 16, 0, 16, 11, 9, 10, 5, 8, 11, 2, 9, 8, 0, 0, 10, 9, 0, 8, 11, 9, 10, 0, 10, 10, 0, 8, 12, 0, 10, 11, 10, 2, 7, 43, 13, 0, 12, 1, 10, 18, 10, 13, 8, 43, 12, 9, 11, 1, 10, 11, 9, 6, 12, 5, 6, 9, 5, 9, 7, 0, 9, 11, 0, 11, 10, 7, 9, 5, 7, 10, 39, 351, 45, 6, 0, 9, 3, 0, 45, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 16; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-nested/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested/qualification.json new file mode 100644 index 00000000..4e7816e7 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "nestedStrings--stringConcealing--53", + "case": "nestedStrings", + "profile": "stringConcealing", + "seed": 53, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": "id:7" + }, + "encodedObservation": { + "kind": "value", + "value": "id:7" + }, + "decodedObservation": { + "kind": "value", + "value": "id:7" + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-nested/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested/source.js new file mode 100644 index 00000000..fbfe36eb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-nested/source.js @@ -0,0 +1 @@ +function outer(prefix) { return function middle(n) { return function inner() { return prefix + ":" + n; }; }; } window.TEST_OUTPUT = outer("id")(7)(); diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-switch/encoded.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-switch/encoded.js new file mode 100644 index 00000000..0a7b1cda --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-switch/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"rBjReGOLJID5p3+s8ZjtBlSQUR/dE9paQjgCi5R6E8yyRcK0A6W/y6UajI0/4KrHuKIgrzEFn81d/2U5NgjDW1EkyQL0UmWYqTRiUquRd6obhcIa4ENQM1Hl61YHwBNJ8FLV3fMHMqWfdRxJm4Ja46vukm+Kz5o29AlTLboI4K2WkW0nqvR9SO47DvjuVcC05pCDaIKb1ABnlDdTfE2g+Vk17OopW7YZSAxO9q8Mg2Msem20cl29p6Xk2CKeyNSz8MQTAPYmDRcuckQuC8eTUaEP126euu4ofcRxGD86iq6f44xFDQ14JV5B5OXguSH5pyo9A9LUuEtJtkH6JTo7UVV8dvQYDMyLAUmWNvmcibTFaI7rFijYsWdYjFZ2Ler6u/W5dnqsQKCrdHWzltauLeiKVeowmWCsU6m3J4RG34G1pEcvwBAHR9pQTFbAn1baXiyFh2afkgEa/YT2Z1bpHqOsmqN4JHHPdopF5kD2DBlqT4uN9p7sLSPoPiKce3TNKZ2npqlw3r2sya4s/Wjh8MyLNge6uDYRv5kDYYDoGFPht9GLxf/NZNTQJI7w68wkY5cHYZL58BWdV/V+evuEbxebbjF6iSkTeHuGdN/EHw6KOgu1wrfuo4Cv0bvnISK7hT2Cuh8GmyghIsab9uqO/q28+Qk5tIfaVHx1YHepRnnizXADpPcAr7A7Cha+7aotu8XYiXhzy5Lr5HCA6F02JcyVGNqpcUGU3spSs3/BJ9jbZNFwtxgLBh5RiJO7Ewcr/B/YxNQp/Uvij+HjzkTF6uvvhsYhppmOr1JMhM4tecPQ3Ss9vc95NoURRWe9Av7C2UwTbIU1s4lRazdsdBo20IZTqwPsbmQtJTOq0bAkiy+f2TMBqT9o9NX8ehqE7bXEBua5EkzwtR6eBg==", /* 1 */"window", /* 2 */1, /* 3 */3, /* 4 */undefined, /* 5 */2, /* 6 */"atob", /* 7 */"", /* 8 */0, /* 9 */"length", /* 10 */"String", /* 11 */"fromCharCode", /* 12 */"charCodeAt", /* 13 */8, /* 14 */2654435769, /* 15 */13, /* 16 */65535]; +var BYTECODE = [47, 5, 194, 1, 12, 0, 0, 0, 6, 0, 0, 42, 4, 5, 1, 6, 47, 7, 331, 4, 14, 0, 0, 4, 1, 47, 11, 91, 1, 13, 1, 0, 1, 4, 5, 2, 11, 2, 11, 1, 0, 1, 8, 883262613, 1, 9, 198, 1, 10, 11, 42, 3, 7, 4, 4, 8, 9, 10, 0, 12, 2, 0, 42, 13, 2, 1, 12, 0, 12, 3, 0, 42, 14, 2, 1, 12, 48, 12, 2, 13, 14, 9, 11, 3, 12, 0, 11, 4, 0, 45, 11, 3, 6, 0, 47, 7, 331, 4, 14, 0, 0, 4, 2, 0, 11, 2, 0, 26, 12, 0, 11, 40, 12, 116, 39, 131, 0, 11, 5, 0, 26, 12, 0, 11, 40, 12, 129, 39, 150, 39, 169, 1, 8, 4065695261, 1, 9, 209, 1, 10, 3, 42, 3, 7, 4, 6, 8, 9, 10, 45, 3, 1, 8, 1523115356, 1, 9, 216, 1, 10, 3, 42, 4, 7, 4, 6, 8, 9, 10, 45, 4, 1, 8, 3699422491, 1, 9, 222, 1, 10, 5, 42, 5, 7, 4, 6, 8, 9, 10, 45, 5, 0, 11, 4, 0, 45, 11, 4, 2, 2, 6, 6, 0, 42, 7, 6, 1, 0, 5, 3, 7, 0, 6, 7, 0, 5, 4, 6, 0, 6, 8, 0, 5, 5, 6, 0, 6, 9, 0, 8, 7, 3, 6, 22, 6, 5, 7, 40, 6, 323, 2, 6, 10, 0, 0, 7, 11, 0, 8, 8, 6, 7, 0, 7, 12, 0, 8, 9, 3, 7, 43, 7, 3, 9, 1, 5, 0, 9, 12, 0, 8, 10, 3, 9, 0, 9, 2, 0, 11, 11, 5, 9, 43, 9, 3, 10, 1, 11, 0, 10, 13, 0, 19, 11, 9, 10, 17, 9, 7, 11, 43, 7, 6, 8, 1, 9, 11, 6, 4, 7, 5, 4, 6, 0, 6, 5, 0, 11, 7, 5, 6, 5, 5, 7, 39, 222, 45, 4, 0, 6, 4, 0, 45, 6, 4, 5, 0, 9, 7, 0, 5, 6, 9, 0, 9, 8, 0, 5, 7, 9, 22, 9, 7, 3, 40, 9, 459, 0, 9, 14, 0, 11, 10, 1, 9, 0, 9, 8, 0, 17, 11, 10, 9, 5, 1, 11, 0, 9, 15, 0, 21, 10, 1, 9, 18, 9, 1, 10, 0, 10, 16, 0, 16, 11, 9, 10, 5, 8, 11, 2, 9, 10, 0, 0, 10, 11, 0, 8, 11, 9, 10, 0, 10, 12, 0, 8, 12, 0, 10, 11, 10, 2, 7, 43, 13, 0, 12, 1, 10, 18, 10, 13, 8, 43, 12, 9, 11, 1, 10, 11, 9, 6, 12, 5, 6, 9, 5, 9, 7, 0, 9, 2, 0, 11, 10, 7, 9, 5, 7, 10, 39, 347, 45, 6, 0, 9, 4, 0, 45, 9]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 15; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-switch/qualification.json b/test/vm/jsconfuser-vm/fixtures/string-concealing-switch/qualification.json new file mode 100644 index 00000000..5ca314c3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-switch/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "switch--stringConcealing--47", + "case": "switch", + "profile": "stringConcealing", + "seed": 47, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": true, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "one", + "other" + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/string-concealing-switch/source.js b/test/vm/jsconfuser-vm/fixtures/string-concealing-switch/source.js new file mode 100644 index 00000000..98a67db9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/string-concealing-switch/source.js @@ -0,0 +1 @@ +function classify(value) { switch (value) { case 1: return "one"; case 2: return "two"; default: return "other"; } } window.TEST_OUTPUT = [classify(1), classify(3)]; diff --git a/test/vm/jsconfuser-vm/integration/harness.js b/test/vm/jsconfuser-vm/integration/harness.js new file mode 100644 index 00000000..496f2ab1 --- /dev/null +++ b/test/vm/jsconfuser-vm/integration/harness.js @@ -0,0 +1,690 @@ +import crypto from 'node:crypto' +import fs from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import { expectConsistentState } from '../../../helper.js' + +const moduleDirectory = path.dirname(fileURLToPath(import.meta.url)) +const repositoryRoot = path.resolve(moduleDirectory, '../../../..') +const corpusRoot = path.join( + repositoryRoot, + 'test/vm/jsconfuser-vm/fixtures/corpus', +) +const evidenceRoot = fs.mkdtempSync( + path.join(tmpdir(), 'jsconfuser-vm-harness-'), +) +const attemptRoot = path.join(evidenceRoot, 'attempts') + +const parserOptions = { + allowReturnOutsideFunction: true, + errorRecovery: false, + sourceType: 'script', +} + +const vmRoleNames = new Set(['Closure', 'VM', 'Upvalue', 'decodeBytecode']) +const wordcodeRoleNames = new Set([ + 'BYTECODE', + 'CONSTANTS', + 'ENCODE_BYTECODE', + 'MAIN_REG_COUNT', + 'MAIN_START_PC', + 'OP', + 'SENTINELS', + 'SLOTS', +]) +const runtimeRoleNames = new Set([ + 'FRAME_START', + 'HEADER_SIZE', + 'TIMING_CHECKS', + 'decodeBytecode', + 'Closure', + 'VM', + 'Upvalue', +]) + +export class HarnessError extends Error { + constructor(code, message, details = {}) { + super(message) + this.name = 'HarnessError' + this.code = code + this.details = details + } +} + +function fail(code, message, details = {}) { + throw new HarnessError(code, message, details) +} + +function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) + return value + Object.freeze(value) + for (const child of Object.values(value)) deepFreeze(child) + return value +} + +function safeSegment(value, label) { + if ( + typeof value !== 'string' || + !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) + ) { + fail('unsafe-path-segment', `${label} is not a safe path segment`, { + value, + }) + } + return value +} + +function readJson(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) + } catch (error) { + fail('fixture-read', `Could not read JSON fixture ${filePath}`, { + cause: error.message, + filePath, + }) + } +} + +function parseSource(source, label, code = 'parse-input') { + try { + return parse(source, parserOptions) + } catch (error) { + fail(code, `Could not parse ${label}`, { cause: error.message }) + } +} + +function getInitializers(ast, name) { + const initializers = [] + traverse(ast, { + VariableDeclarator(path) { + if (path.node.id.type === 'Identifier' && path.node.id.name === name) { + initializers.push(path.node.init) + } + }, + }) + return initializers +} + +function getSingleInitializer(ast, name) { + const initializers = getInitializers(ast, name) + if (initializers.length !== 1) { + fail('container-role', `Expected one ${name} declaration`, { + count: initializers.length, + name, + }) + } + return initializers[0] +} + +export function parseNumericContainer(source) { + const ast = parseSource(source, 'encoded.js') + const encodeBytecode = getSingleInitializer(ast, 'ENCODE_BYTECODE') + if (!encodeBytecode || encodeBytecode.type !== 'BooleanLiteral') { + fail('container-role', 'ENCODE_BYTECODE is not a boolean literal') + } + if (encodeBytecode.value === true) { + fail( + 'unsupported-encoded-bytecode', + 'ENCODE_BYTECODE=true is an explicit decline for the numeric-wordcode harness', + ) + } + + const bytecode = getSingleInitializer(ast, 'BYTECODE') + if (!bytecode || bytecode.type !== 'ArrayExpression') { + fail('container-role', 'BYTECODE is not an array expression') + } + const words = bytecode.elements.map((element, index) => { + if ( + !element || + element.type !== 'NumericLiteral' || + !Number.isSafeInteger(element.value) || + element.value < 0 || + element.value > 0xffffffff + ) { + fail( + 'non-numeric-wordcode', + `BYTECODE word ${index} is not an unsigned 32-bit integer`, + { + index, + }, + ) + } + return element.value + }) + + return { bytecode: Object.freeze(words), encodeBytecode: false } +} + +function startsWithLeadingDebugComment(source) { + const firstLineEnd = source.indexOf('\n') + const firstLine = firstLineEnd === -1 ? source : source.slice(0, firstLineEnd) + return firstLine.startsWith('//') +} + +function stripLeadingDebugComments(source) { + let offset = 0 + while (offset < source.length) { + const lineEnd = source.indexOf('\n', offset) + const line = + lineEnd === -1 ? source.slice(offset) : source.slice(offset, lineEnd) + if (!line.startsWith('//')) break + offset = lineEnd === -1 ? source.length : lineEnd + 1 + } + return source.slice(offset) +} + +function sha256(source) { + return crypto.createHash('sha256').update(source).digest('hex') +} + +function stableCellPaths(root, kind, cellId) { + const relativeDirectory = path.join( + 'test/vm/jsconfuser-vm/fixtures/corpus', + kind, + cellId, + ) + const directory = path.join(root, relativeDirectory) + return deepFreeze({ + directory, + encoded: path.join(directory, 'encoded.js'), + oracle: path.join(directory, 'oracle.json'), + source: path.join(directory, 'source.js'), + }) +} + +export function loadVmCell( + cellId, + { kind = 'raw', root = repositoryRoot } = {}, +) { + safeSegment(cellId, 'cellId') + if (kind !== 'raw' && kind !== 'focused') { + fail( + 'unsupported-corpus-kind', + `Unsupported js-confuser-vm corpus kind: ${kind}`, + { kind }, + ) + } + + const paths = stableCellPaths(root, kind, cellId) + const encodedSource = fs.readFileSync(paths.encoded, 'utf8') + if (startsWithLeadingDebugComment(encodedSource)) { + fail( + 'debug-input', + 'The harness accepts only comment-stripped encoded.js input', + ) + } + const source = fs.readFileSync(paths.source, 'utf8') + const oracle = readJson(paths.oracle) + if (oracle.cellId !== cellId) { + fail( + 'fixture-metadata', + 'Oracle metadata does not match the requested cell', + { + cellId, + oracleCellId: oracle.cellId, + }, + ) + } + const container = parseNumericContainer(encodedSource) + + return deepFreeze({ + bytecode: container.bytecode, + cellId, + corpusKind: kind, + encodedSource, + oracle, + paths, + source, + }) +} + +export function parseSuccessfulOutput(output) { + if (typeof output !== 'string') { + fail('non-string-output', 'Successful recovery output must be a string') + } + return parseSource(output, 'recovered output', 'parse-output') +} + +function addFinding(findings, bucket, name) { + findings[bucket].add(name) +} + +export function findVmResidue(ast) { + const findings = { + runtime: new Set(), + vm: new Set(), + wordcode: new Set(), + } + + traverse(ast, { + Identifier(path) { + const name = path.node.name + if (vmRoleNames.has(name)) addFinding(findings, 'vm', name) + if (wordcodeRoleNames.has(name)) addFinding(findings, 'wordcode', name) + if (runtimeRoleNames.has(name)) addFinding(findings, 'runtime', name) + }, + MemberExpression(path) { + if ( + !path.node.computed && + path.node.object.type === 'Identifier' && + path.node.object.name === 'vm' && + path.node.property.type === 'Identifier' && + path.node.property.name === 'run' + ) { + addFinding(findings, 'runtime', 'vm.run') + } + }, + NewExpression(path) { + if ( + path.node.callee.type === 'Identifier' && + path.node.callee.name === 'VM' + ) { + addFinding(findings, 'runtime', 'new VM') + } + }, + }) + + return { + runtime: [...findings.runtime].sort(), + vm: [...findings.vm].sort(), + wordcode: [...findings.wordcode].sort(), + } +} + +export function checkVmResidue(astOrSource) { + const ast = + typeof astOrSource === 'string' + ? parseSuccessfulOutput(astOrSource) + : astOrSource + if (!ast || typeof ast !== 'object') + fail('invalid-ast', 'VM residue check needs a Babel AST or source') + const findings = findVmResidue(ast) + return { + ...findings, + ok: Object.values(findings).every((bucket) => bucket.length === 0), + } +} + +export function assertNoVmResidue(astOrSource) { + const findings = checkVmResidue(astOrSource) + if (!findings.ok) { + fail( + 'vm-residue', + 'Recovered output still contains VM, wordcode, or runtime residue', + findings, + ) + } + return findings +} + +export function assertDerivedStateConsistency( + ast, + output, + parseOptions = parserOptions, +) { + try { + expectConsistentState(ast, output, parseOptions) + } catch (error) { + fail( + 'derived-state', + 'Recovered AST derived state is inconsistent with its output', + { + cause: error.message, + }, + ) + } + return { ok: true } +} + +function validateRecoveryResult(result) { + if (result === null) return { output: null, status: 'declined' } + if (result === undefined) + fail( + 'undefined-recovery', + 'Decoder returned undefined instead of string or null', + ) + if (typeof result !== 'string') { + if (typeof result === 'object' || typeof result === 'function') { + fail( + 'malformed-recovery', + 'Decoder returned a malformed recovery result', + { + type: typeof result, + }, + ) + } + fail( + 'non-string-output', + 'Decoder returned a non-string, non-null recovery result', + { + type: typeof result, + }, + ) + } + return { output: result, status: 'success' } +} + +function invokeDecoder(decoder, input, context) { + if (typeof decoder !== 'function') { + fail( + 'missing-decoder', + 'A standalone decoder/recovery function must be injected', + ) + } + try { + return validateRecoveryResult(decoder(input, context)) + } catch (error) { + if (error instanceof HarnessError) throw error + fail( + 'decoder-threw', + 'Injected decoder threw before producing a recovery result', + { + cause: error.message, + }, + ) + } +} + +function canonicalRecovery(result) { + if (result.output === null) return result + return { + output: stripLeadingDebugComments(result.output), + status: result.status, + } +} + +export function inspectDebugCommentDependence(decoder, input) { + const context = Object.freeze({ + purpose: 'synthetic-debug-comment-independence-check', + }) + const plain = invokeDecoder(decoder, input, context) + const syntheticDebugInput = `// __A0_SYNTHETIC_DEBUG_COMMENT__\n${input}` + const withDebug = invokeDecoder(decoder, syntheticDebugInput, context) + const plainCanonical = canonicalRecovery(plain) + const debugCanonical = canonicalRecovery(withDebug) + const dependent = + plainCanonical.status !== debugCanonical.status || + plainCanonical.output !== debugCanonical.output + return { + dependent, + inputWasCommentStripped: !startsWithLeadingDebugComment(input), + syntheticDebugInput, + } +} + +export function assertDebugCommentIndependent(decoder, input) { + const check = inspectDebugCommentDependence(decoder, input) + if (check.dependent) { + fail( + 'debug-comment-dependence', + 'Decoder result changes when a synthetic debug comment is added', + ) + } + return check +} + +export async function runBoundedBehavioralOracle( + oracle, + request, + { timeoutMs = 2000 } = {}, +) { + if (typeof oracle !== 'function') { + fail( + 'missing-oracle', + 'Behavioral oracle seam needs a function when enabled', + ) + } + if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) { + fail( + 'invalid-oracle-timeout', + 'Behavioral oracle timeout must be a positive integer', + { timeoutMs }, + ) + } + + const controller = new AbortController() + let timer + let timedOut = false + const oraclePromise = Promise.resolve().then(() => + oracle(request, controller.signal), + ) + const timeoutPromise = new Promise((resolve) => { + timer = setTimeout(() => { + timedOut = true + controller.abort() + resolve({ kind: 'timeout' }) + }, timeoutMs) + }) + const settledOracle = oraclePromise.then( + (value) => ({ kind: 'value', value }), + (error) => ({ kind: 'error', error }), + ) + const winner = await Promise.race([settledOracle, timeoutPromise]) + clearTimeout(timer) + + if (winner.kind === 'timeout') return { status: 'timed-out', timedOut: true } + if (winner.kind === 'error') { + return { cause: winner.error.message, status: 'failed', timedOut } + } + if (winner.value === true || (winner.value && winner.value.ok === true)) { + return { status: 'passed', timedOut } + } + return { status: 'failed', value: winner.value, timedOut } +} + +function attemptPathsFor(cell, name, root = repositoryRoot) { + safeSegment(cell.cellId, 'cellId') + safeSegment(name, 'attempt name') + const directory = path.join( + attemptRootFor(root), + cell.corpusKind, + cell.cellId, + ) + return { + directory, + output: path.join(directory, `${name}.js`), + result: path.join(directory, `${name}.json`), + } +} + +function attemptRootFor(root) { + return root === repositoryRoot + ? attemptRoot + : path.join(root, 'jsconfuser-vm-attempts') +} + +export function checkAttempt(attempt) { + if (!attempt || !attempt.outputPath || !attempt.resultPath) { + fail('invalid-attempt', 'Attempt check needs a result and output path') + } + let output + let recorded + try { + output = fs.readFileSync(attempt.outputPath, 'utf8') + recorded = readJson(attempt.resultPath) + } catch (error) { + if (error instanceof HarnessError) throw error + fail('attempt-read', 'Could not read named attempt artifacts', { + cause: error.message, + }) + } + if (attempt.status === 'declined' && output !== attempt.input) { + fail( + 'decline-output-mismatch', + 'Declined attempt output is not byte-for-byte original input', + ) + } + if (output !== attempt.output) { + fail( + 'attempt-output-mismatch', + 'Attempt output changed after it was written', + ) + } + const expectedOutputPath = path.relative( + attempt.repositoryRoot, + attempt.outputPath, + ) + const expectedResultPath = path.relative( + attempt.repositoryRoot, + attempt.resultPath, + ) + if ( + recorded.schemaVersion !== 'a0-attempt.v1' || + recorded.attemptName !== attempt.name || + recorded.caseId !== attempt.cell.cellId || + recorded.corpusKind !== attempt.cell.corpusKind || + recorded.status !== attempt.status || + recorded.inputSha256 !== sha256(attempt.input) || + recorded.outputPath !== expectedOutputPath || + recorded.resultPath !== expectedResultPath + ) { + fail( + 'attempt-result-mismatch', + 'Named attempt result does not describe its artifacts', + ) + } + if ( + recorded.outputBytes !== Buffer.byteLength(output) || + recorded.outputSha256 !== sha256(output) + ) { + fail( + 'attempt-result-mismatch', + 'Named attempt result has the wrong output hash', + ) + } + return { + ok: true, + outputPath: attempt.outputPath, + resultPath: attempt.resultPath, + } +} + +async function writeAttempt({ + cell, + name, + input, + output, + status, + oracleResult, + repository, +}) { + const paths = attemptPathsFor(cell, name, repository) + fs.mkdirSync(paths.directory, { recursive: true }) + fs.writeFileSync(paths.output, output, 'utf8') + const record = { + schemaVersion: 'a0-attempt.v1', + attemptName: name, + caseId: cell.cellId, + corpusKind: cell.corpusKind, + inputSha256: sha256(input), + outputBytes: Buffer.byteLength(output), + outputPath: path.relative(repository, paths.output), + outputSha256: sha256(output), + resultPath: path.relative(repository, paths.result), + oracle: oracleResult + ? { + invoked: true, + status: oracleResult.status, + timedOut: oracleResult.timedOut, + } + : { invoked: false }, + status, + } + fs.writeFileSync(paths.result, `${JSON.stringify(record, null, 2)}\n`, 'utf8') + return { + cell, + input, + name, + oracleResult, + output, + outputPath: paths.output, + repositoryRoot: repository, + result: record, + resultPath: paths.result, + status, + } +} + +export async function runAttempt( + cell, + { + behavioralOracle, + decoder, + expectedStatus, + input = cell.encodedSource, + name, + oracleTimeoutMs = cell.oracle.timeoutMs ?? 2000, + root = repositoryRoot, + }, +) { + if (!cell || typeof cell.encodedSource !== 'string') { + fail('invalid-cell', 'Attempt needs a loaded js-confuser-vm cell') + } + safeSegment(name, 'attempt name') + if (typeof input !== 'string') + fail('invalid-input', 'Attempt input must be a string') + const context = Object.freeze({ + bytecode: cell.bytecode, + cell, + input, + oracle: cell.oracle, + source: cell.source, + }) + const recovery = invokeDecoder(decoder, input, context) + const output = recovery.status === 'declined' ? input : recovery.output + let ast + let oracleResult + + if (recovery.status === 'success') { + ast = parseSuccessfulOutput(output) + assertNoVmResidue(ast) + assertDerivedStateConsistency(ast, output) + if (behavioralOracle) { + oracleResult = await runBoundedBehavioralOracle( + behavioralOracle, + Object.freeze({ + metadata: cell.oracle, + recoveredSource: output, + source: cell.source, + }), + { timeoutMs: oracleTimeoutMs }, + ) + if (oracleResult.status !== 'passed') { + fail( + 'behavioral-oracle', + 'Behavioral oracle did not complete successfully', + oracleResult, + ) + } + } + } + + if (expectedStatus && expectedStatus !== recovery.status) { + fail( + 'unexpected-status', + `Expected ${expectedStatus} but recovery was ${recovery.status}`, + ) + } + const attempt = await writeAttempt({ + cell, + input, + name, + oracleResult, + output, + repository: root, + status: recovery.status, + }) + checkAttempt(attempt) + return { ...attempt, ast } +} + +export const paths = deepFreeze({ + attemptRoot, + corpusRoot, + evidenceRoot, + repositoryRoot, +}) diff --git a/test/vm/jsconfuser-vm/integration/readability-metrics.js b/test/vm/jsconfuser-vm/integration/readability-metrics.js new file mode 100644 index 00000000..7eca9ba3 --- /dev/null +++ b/test/vm/jsconfuser-vm/integration/readability-metrics.js @@ -0,0 +1,376 @@ +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { parse } from '@babel/parser' +import traverseModule from '@babel/traverse' +import * as t from '@babel/types' +import { diagnoseStandalone } from '../../../../src/vm/jsconfuser-vm/decode-standalone.js' + +const traverse = traverseModule.default || traverseModule +const fixtureRoot = path.join( + path.dirname(fileURLToPath(import.meta.url)), + '../fixtures/readability', +) +const parserOptions = { + allowReturnOutsideFunction: true, + errorRecovery: false, + sourceType: 'script', +} + +const functionTypes = new Set([ + 'ArrowFunctionExpression', + 'FunctionDeclaration', + 'FunctionExpression', +]) + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) +} + +function readText(filePath) { + return fs.readFileSync(filePath, 'utf8') +} + +function digest(value) { + return crypto.createHash('sha256').update(value).digest('hex') +} + +function parseSource(source) { + return parse(source, parserOptions) +} + +function walk(node, visit, seen = new Set()) { + if (!node || typeof node.type !== 'string' || seen.has(node)) return + seen.add(node) + visit(node) + for (const key of t.VISITOR_KEYS[node.type] || []) { + const value = node[key] + if (Array.isArray(value)) { + for (const child of value) walk(child, visit, seen) + } else { + walk(value, visit, seen) + } + } +} + +function generatedFamily(name) { + if (!name.startsWith('__recovered_')) return null + if (name.startsWith('__recovered_function_')) return 'function' + if (name.startsWith('__recovered_r_')) return 'register' + if (name.startsWith('__recovered_cells_')) return 'cell' + if ( + name.startsWith('__recovered_captures') || + name.startsWith('__recovered_closure_captures_') + ) + return 'capture' + if ( + name.startsWith('__recovered_handler') || + name.startsWith('__recovered_error') + ) + return 'handler' + if ( + name.startsWith('__recovered_pc') || + name.startsWith('__recovered_dispatch') || + name.startsWith('__recovered_loop_') + ) + return 'control' + if ( + name.startsWith('__recovered_descriptor_') || + name.startsWith('__recovered_existing_') + ) + return 'descriptor' + if ( + name.startsWith('__recovered_args') || + name.startsWith('__recovered_this') + ) + return 'call-wrapper' + if (name === '__recovered_enumerable_keys') return 'helper' + return 'other' +} + +function identifierMetrics(ast) { + const namesByFamily = new Map() + let total = 0 + walk(ast, (node) => { + if (node.type !== 'Identifier') return + const family = generatedFamily(node.name) + if (!family) return + total += 1 + if (!namesByFamily.has(family)) namesByFamily.set(family, new Set()) + namesByFamily.get(family).add(node.name) + }) + const families = {} + for (const family of [...namesByFamily.keys()].sort()) { + const names = namesByFamily.get(family) + families[family] = { distinct: names.size, total: 0 } + } + walk(ast, (node) => { + if (node.type !== 'Identifier') return + const family = generatedFamily(node.name) + if (family) families[family].total += 1 + }) + return { + distinct: [...namesByFamily.values()].reduce((n, set) => n + set.size, 0), + total, + families, + } +} + +function hasGeneratedIdentifier(node) { + let found = false + walk(node, (child) => { + if (child.type === 'Identifier' && generatedFamily(child.name)) found = true + }) + return found +} + +function isStatement(node) { + return node.type.endsWith('Statement') || node.type.endsWith('Declaration') +} + +function astMetrics(ast) { + let statements = 0 + let generatedStatements = 0 + let functions = 0 + let declarations = 0 + let emptyCellDeclarations = 0 + walk(ast, (node) => { + if (isStatement(node)) { + statements += 1 + if (hasGeneratedIdentifier(node)) generatedStatements += 1 + } + if (functionTypes.has(node.type)) functions += 1 + if ( + node.type === 'VariableDeclarator' && + node.id?.type === 'Identifier' && + node.id.name.startsWith('__recovered_cells_') && + node.init?.type === 'ArrayExpression' && + node.init.elements.length === 0 + ) + emptyCellDeclarations += 1 + }) + const bindings = new Set() + const generatedBindings = [] + traverse(ast, { + Scopable(scopePath) { + for (const binding of Object.values(scopePath.scope.bindings)) { + if (bindings.has(binding)) continue + bindings.add(binding) + declarations += 1 + if (generatedFamily(binding.identifier.name)) { + generatedBindings.push({ + name: binding.identifier.name, + references: binding.referencePaths.length, + }) + } + } + }, + }) + const generatedDeclarationsByFamily = {} + for (const binding of generatedBindings) { + const family = generatedFamily(binding.name) + if (!generatedDeclarationsByFamily[family]) + generatedDeclarationsByFamily[family] = { total: 0, zeroReferences: 0 } + generatedDeclarationsByFamily[family].total += 1 + if (binding.references === 0) + generatedDeclarationsByFamily[family].zeroReferences += 1 + } + return { + statements, + generatedStatements, + functions, + declaredIdentifiers: declarations, + generatedDeclarations: { + total: generatedBindings.length, + zeroReferences: generatedBindings.filter( + ({ references }) => references === 0, + ).length, + }, + generatedDeclarationsByFamily, + emptyCellDeclarations, + } +} + +function assignmentParts(statement) { + const expression = statement?.expression + if ( + statement?.type !== 'ExpressionStatement' || + expression?.type !== 'AssignmentExpression' || + expression.operator !== '=' || + expression.left?.type !== 'Identifier' + ) + return null + return { + destination: expression.left.name, + source: + expression.right?.type === 'Identifier' ? expression.right.name : null, + } +} + +function adjacentIdentifierCopies(ast) { + let copies = 0 + walk(ast, (node) => { + if (!['Program', 'BlockStatement'].includes(node.type)) return + for (let index = 0; index + 1 < node.body.length; index += 1) { + const producer = assignmentParts(node.body[index]) + const move = assignmentParts(node.body[index + 1]) + if (producer?.destination && producer.destination === move?.source) + copies += 1 + } + }) + return copies +} + +function memberParts(node) { + if ( + node?.type !== 'MemberExpression' || + node.computed || + node.object?.type !== 'Identifier' || + node.property?.type !== 'Identifier' + ) + return null + return { object: node.object.name, property: node.property.name } +} + +function operationMetrics(ast) { + const reflectCalls = {} + let computedGlobalThis = 0 + let descriptorCalls = 0 + let closureWrappers = 0 + let handlerOperations = 0 + let enumerableHelperDeclarations = 0 + let enumerableHelperCalls = 0 + let structuredFunctions = 0 + + walk(ast, (node) => { + const parts = memberParts(node) + if (parts?.object === 'globalThis') computedGlobalThis += 1 + if ( + parts?.object === '__recovered_handlers' || + parts?.object === '__recovered_handler' + ) + handlerOperations += 1 + if (parts?.object === 'Reflect') { + reflectCalls[parts.property] = (reflectCalls[parts.property] || 0) + 1 + } + if ( + parts?.object === 'Object' && + ['defineProperty', 'getOwnPropertyDescriptor'].includes(parts.property) + ) + descriptorCalls += 1 + if ( + node.type === 'FunctionDeclaration' && + node.id?.name === '__recovered_enumerable_keys' + ) + enumerableHelperDeclarations += 1 + if ( + node.type === 'CallExpression' && + node.callee?.type === 'Identifier' && + node.callee.name === '__recovered_enumerable_keys' + ) + enumerableHelperCalls += 1 + if (node.type === 'FunctionExpression' && hasGeneratedIdentifier(node)) { + if (hasName(node, '__recovered_closure_args')) closureWrappers += 1 + } + if ( + node.type !== 'FunctionDeclaration' || + !node.id?.name.startsWith('__recovered_function_') + ) + return + structuredFunctions += 1 + }) + + return { + reflectCalls, + computedGlobalThis, + descriptorCalls: Math.max( + 0, + descriptorCalls - enumerableHelperDeclarations, + ), + closureWrappers, + handlerOperations, + structuredFunctions, + enumerableHelper: { + declarations: enumerableHelperDeclarations, + calls: enumerableHelperCalls, + unused: enumerableHelperDeclarations > 0 && enumerableHelperCalls === 0, + }, + } +} + +function hasName(node, name) { + let found = false + walk(node, (child) => { + if (child.type === 'Identifier' && child.name === name) found = true + }) + return found +} + +function roundRatio(numerator, denominator) { + if (denominator === 0) return null + return Number((numerator / denominator).toFixed(6)) +} + +function fileMetrics(source) { + return { bytes: Buffer.byteLength(source), sha256: digest(source) } +} + +function caseMetrics(caseId) { + const directory = path.join(fixtureRoot, caseId) + const source = readText(path.join(directory, 'source.js')) + const encoded = readText(path.join(directory, 'encoded.js')) + const decoded = readText(path.join(directory, 'decoded.js')) + const diagnosis = diagnoseStandalone(encoded) + if (!diagnosis.ok) + throw new Error( + `${caseId} no longer decodes: ${diagnosis.diagnostic.code}: ${diagnosis.diagnostic.message}`, + ) + if (diagnosis.result.output !== decoded) + throw new Error(`${caseId} decoded baseline does not match current output`) + + const sourceAst = parseSource(source) + const encodedAst = parseSource(encoded) + const decodedAst = parseSource(decoded) + const sourceFile = fileMetrics(source) + const encodedFile = fileMetrics(encoded) + const decodedFile = fileMetrics(decoded) + const decodedAstMetrics = astMetrics(decodedAst) + return { + caseId, + status: 'success', + files: { source: sourceFile, encoded: encodedFile, decoded: decodedFile }, + ratios: { + decodedToSource: roundRatio(decodedFile.bytes, sourceFile.bytes), + decodedToEncoded: roundRatio(decodedFile.bytes, encodedFile.bytes), + }, + ast: { + source: astMetrics(sourceAst), + encoded: astMetrics(encodedAst), + decoded: decodedAstMetrics, + }, + generatedIdentifiers: identifierMetrics(decodedAst), + operations: operationMetrics(decodedAst), + adjacentIdentifierCopies: adjacentIdentifierCopies(decodedAst), + } +} + +export function collectMetrics() { + const manifest = readJson(path.join(fixtureRoot, 'manifest.json')) + const cases = [...manifest.cases].sort() + return { + schemaVersion: 'jsconfuser-vm-readability-metrics.v1', + fixtureSchemaVersion: manifest.schemaVersion, + cases: cases.map(caseMetrics), + } +} + +export function loadBaseline() { + return readJson(path.join(fixtureRoot, 'baseline.json')) +} + +const isMain = + process.argv[1] && + path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url)) +if (isMain) + process.stdout.write(`${JSON.stringify(collectMetrics(), null, 2)}\n`) diff --git a/test/vm/jsconfuser-vm/integration/readability.test.js b/test/vm/jsconfuser-vm/integration/readability.test.js new file mode 100644 index 00000000..04b9e5f9 --- /dev/null +++ b/test/vm/jsconfuser-vm/integration/readability.test.js @@ -0,0 +1,196 @@ +import fs from 'node:fs' +import path from 'node:path' +import vm from 'node:vm' +import { fileURLToPath } from 'node:url' +import { parse } from '@babel/parser' +import { expect, test } from 'vitest' +import { expectConsistentState } from '../../../helper.js' +import { diagnoseStandalone } from '../../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { collectMetrics, loadBaseline } from './readability-metrics.js' + +const fixtureRoot = path.join( + path.dirname(fileURLToPath(import.meta.url)), + '../fixtures/readability', +) +const parserOptions = { + allowReturnOutsideFunction: true, + errorRecovery: false, + sourceType: 'script', +} + +function execute(filePath) { + const context = {} + context.window = context + vm.createContext(context) + vm.runInContext(fs.readFileSync(filePath, 'utf8'), context, { + filename: filePath, + timeout: 5000, + }) + return context.TEST_OUTPUT +} + +function outputFunction(output, functionId) { + const start = output.indexOf(`function __recovered_function_${functionId}(`) + expect(start).toBeGreaterThanOrEqual(0) + const next = output.indexOf('\n function __recovered_function_', start + 1) + return output.slice(start, next === -1 ? output.length : next) +} + +function decodedOutput(caseId) { + return fs.readFileSync(path.join(fixtureRoot, caseId, 'decoded.js'), 'utf8') +} + +function baselineByCase() { + return new Map(loadBaseline().cases.map((entry) => [entry.caseId, entry])) +} + +test('readability benchmark is tracked, complete, and deterministic', () => { + const first = collectMetrics() + const second = collectMetrics() + + expect(second).toEqual(first) + expect(first.schemaVersion).toBe('jsconfuser-vm-readability-metrics.v1') + expect(first.cases.map(({ caseId }) => caseId)).toEqual([ + 'arithmetic', + 'closure-loop', + 'scale', + ]) + expect(first.cases.every(({ status }) => status === 'success')).toBe(true) + for (const result of first.cases) { + expect(result.files.source.bytes).toBeGreaterThan(0) + expect(result.files.encoded.bytes).toBeGreaterThan( + result.files.source.bytes, + ) + expect(result.files.decoded.bytes).toBeGreaterThan(0) + expect(result.ratios.decodedToSource).toBeTypeOf('number') + expect(result.ratios.decodedToEncoded).toBeTypeOf('number') + expect(result.ast.decoded.statements).toBeGreaterThan(0) + expect(result.ast.decoded.declaredIdentifiers).toBeGreaterThan(0) + } +}) + +test('baseline records the generated VM representation that readability work must address', () => { + const metrics = collectMetrics() + const generatedTotals = metrics.cases.map( + ({ generatedIdentifiers }) => generatedIdentifiers.total, + ) + const generatedStatementTotals = metrics.cases.map( + ({ ast }) => ast.decoded.generatedStatements, + ) + + expect(generatedTotals.every((total) => total > 0)).toBe(true) + expect(generatedStatementTotals.every((total) => total > 0)).toBe(true) + expect( + metrics.cases.every( + ({ operations }) => operations.enumerableHelper.declarations === 0, + ), + ).toBe(true) +}) + +test('retains only setup required by captures, arguments, and enumeration', () => { + const arithmetic = decodedOutput('arithmetic') + expect(outputFunction(arithmetic, 0)).toMatch( + /^function __recovered_function_0\(\)/, + ) + + const closure = decodedOutput('closure-loop') + expect(outputFunction(closure, 0)).toMatch( + /^function __recovered_function_0\(\)/, + ) + expect(outputFunction(closure, 1)).toMatch( + /^function __recovered_function_1\(\.\.\.__recovered_args\)/, + ) + expect(outputFunction(closure, 1)).toContain( + 'const __recovered_cells_1 = [];', + ) + expect(outputFunction(closure, 2)).toMatch( + /^function __recovered_function_2\(__recovered_captures, \.\.\.__recovered_args\)/, + ) + expect(outputFunction(closure, 2)).not.toContain( + 'const __recovered_cells_2 = [];', + ) + + const forInPath = path.join( + fixtureRoot, + '../corpus/raw/f-for-in-enumeration/encoded.js', + ) + const forInDiagnosis = diagnoseStandalone(fs.readFileSync(forInPath, 'utf8')) + expect(forInDiagnosis.ok).toBe(true) + expect(forInDiagnosis.result.output).toContain( + 'function __recovered_enumerable_keys(__value)', + ) +}) + +test('source-oriented output is smaller without changing recovered behavior', () => { + const metrics = collectMetrics() + const baselines = baselineByCase() + const expectedEmptyCells = { + arithmetic: 0, + 'closure-loop': 1, + scale: 0, + } + + for (const result of metrics.cases) { + const baseline = baselines.get(result.caseId) + expect(baseline).toBeDefined() + expect(result.files.decoded.bytes).toBeLessThan(baseline.decoded.bytes) + expect(result.ratios.decodedToSource).toBeLessThan( + baseline.decoded.ratios.decodedToSource, + ) + expect(result.ratios.decodedToEncoded).toBeLessThan( + baseline.decoded.ratios.decodedToEncoded, + ) + expect(result.ast.decoded.statements).toBeLessThan( + baseline.decoded.ast.statements, + ) + expect(result.ast.decoded.generatedStatements).toBeLessThan( + baseline.decoded.ast.generatedStatements, + ) + expect(result.ast.decoded.declaredIdentifiers).toBeLessThan( + baseline.decoded.ast.declaredIdentifiers, + ) + expect(result.ast.decoded.generatedDeclarations.total).toBeLessThan( + baseline.decoded.ast.generatedDeclarations.total, + ) + expect( + result.ast.decoded.generatedDeclarations.zeroReferences, + ).toBeLessThanOrEqual( + baseline.decoded.ast.generatedDeclarations.zeroReferences, + ) + expect(result.ast.decoded.emptyCellDeclarations).toBe( + expectedEmptyCells[result.caseId], + ) + expect(result.generatedIdentifiers.total).toBeLessThan( + baseline.decoded.generatedIdentifiers.total, + ) + expect(result.generatedIdentifiers.distinct).toBeLessThan( + baseline.decoded.generatedIdentifiers.distinct, + ) + expect(result.operations.enumerableHelper).toEqual({ + declarations: 0, + calls: 0, + unused: false, + }) + expect({ + reflectCalls: result.operations.reflectCalls, + computedGlobalThis: result.operations.computedGlobalThis, + descriptorCalls: result.operations.descriptorCalls, + closureWrappers: result.operations.closureWrappers, + handlerOperations: result.operations.handlerOperations, + structuredFunctions: result.operations.structuredFunctions, + }).toEqual(baseline.decoded.operations) + + const sourcePath = path.join(fixtureRoot, result.caseId, 'source.js') + const decodedPath = path.join(fixtureRoot, result.caseId, 'decoded.js') + expect(execute(decodedPath)).toEqual(execute(sourcePath)) + + const decoded = fs.readFileSync(decodedPath, 'utf8') + const ast = parse(decoded, parserOptions) + expect(() => + expectConsistentState(ast, decoded, parserOptions), + ).not.toThrow() + expect(decoded).not.toMatch( + /\b(?:VM|Upvalue|decodeBytecode|BYTECODE|CONSTANTS|wordcode)\b/, + ) + } +}) diff --git a/test/vm/jsconfuser-vm/minified-self-modifying-encoded.test.js b/test/vm/jsconfuser-vm/minified-self-modifying-encoded.test.js new file mode 100644 index 00000000..d064ebaa --- /dev/null +++ b/test/vm/jsconfuser-vm/minified-self-modifying-encoded.test.js @@ -0,0 +1,65 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnosePatchAddedOpcodes } from '../../../src/vm/jsconfuser-vm/observe-patch-added-opcodes.js' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { diagnoseSelfModifyingRegions } from '../../../src/vm/jsconfuser-vm/observe-self-modifying.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(file) { + return fs.readFileSync( + new URL( + `fixtures/minified-self-modifying-encoded-exceptions/${file}`, + import.meta.url, + ), + 'utf8', + ) +} + +const source = fixture('source.js') +const encoded = fixture('encoded.js') +const record = JSON.parse(fixture('qualification.json')) + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, atob, btoa, performance: { now: () => 0 } }, + { timeout: 30000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test('recovers minified encoded PATCH regions before standalone emission', () => { + const container = diagnoseContainer(encoded) + expect(container.ok).toBe(true) + expect(container.result.roles.constructors.VM.name).toBeNull() + expect(container.result.roles.methods.vm.run.name).toBeNull() + expect(diagnosePatchAddedOpcodes(encoded)).toMatchObject({ + ok: true, result: { anti: null, specialized: null, macro: null, alias: null }, + }) + const patches = diagnoseSelfModifyingRegions(container.result) + expect(patches.ok).toBe(true) + expect(patches.result.patches.length).toBeGreaterThan(0) + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(encoded)).toEqual(observe(source)) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(record.status).toBe('accepted') + expect(record.effectObservation.components).toEqual({ + minify: true, + selfModifying: true, + encodeBytecode: true, + }) +}) diff --git a/test/vm/jsconfuser-vm/normalize-alias-combinations.test.js b/test/vm/jsconfuser-vm/normalize-alias-combinations.test.js new file mode 100644 index 00000000..ec67778b --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-alias-combinations.test.js @@ -0,0 +1,230 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseAntiRuntime } from '../../../src/vm/jsconfuser-vm/analyze-anti-runtime.js' +import { diagnoseSpecializedRuntime } from '../../../src/vm/jsconfuser-vm/observe-specialized-runtime.js' +import { diagnoseMacroRuntime } from '../../../src/vm/jsconfuser-vm/observe-macro-runtime.js' +import { diagnoseAliasedRuntime } from '../../../src/vm/jsconfuser-vm/observe-aliased-runtime.js' +import { diagnoseNormalizeAliasedWordcode } from '../../../src/vm/jsconfuser-vm/normalize-aliased-wordcode.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, atob, btoa, performance: { now: () => 0 } }, + { timeout: 30000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +const samples = [ + ['macro-alias-exceptions', ['macroOpcodes', 'aliasedOpcodes']], + [ + 'eight-option-alias-exceptions', + [ + 'stringConcealing', + 'controlFlowFlattening', + 'dispatcher', + 'concealConstants', + 'antiInstrumentation', + 'specializedOpcodes', + 'macroOpcodes', + 'aliasedOpcodes', + ], + ], + [ + 'shuffled-alias-stack-exceptions', + [ + 'stringConcealing', + 'controlFlowFlattening', + 'dispatcher', + 'concealConstants', + 'antiInstrumentation', + 'specializedOpcodes', + 'macroOpcodes', + 'aliasedOpcodes', + 'shuffleOpcodes', + ], + ], +] + +test.each(samples)( + 'normalizes and decodes %s', + (name, options) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const record = JSON.parse(fixture(name, 'qualification.json')) + const container = diagnoseContainer(encoded).result + const before = JSON.stringify(container) + const normalized = diagnoseNormalizeAliasedWordcode(encoded, container, { + allowObservedPrefix: true, + }) + expect(normalized.ok).toBe(true) + expect(normalized.result.aliasedInstructionCount).toBeGreaterThan(0) + expect(normalized.result.macroInstructionCount).toBeGreaterThan(0) + expect(JSON.stringify(container)).toBe(before) + expect(readWordcode(normalized.result.container)).not.toBeNull() + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect( + assertDerivedStateConsistency(output, decoded.result.output).ok, + ).toBe(true) + const expected = observe(source) + expect(observe(encoded)).toEqual(expected) + expect(observe(decoded.result.output)).toEqual(expected) + expect(record.status).toBe('accepted') + expect(record.effectObservation.observed).toBe(true) + for (const option of options) + expect(record.effectObservation.components[option]).toBe(true) + }, +) + +test('declines a changed alias body after a proved macro prefix', () => { + const encoded = fixture('macro-alias-exceptions', 'encoded.js') + const macro = diagnoseMacroRuntime(encoded, { allowAliasTail: true }) + expect(macro.ok).toBe(true) + const alias = diagnoseAliasedRuntime(encoded, { + macroObservation: macro.result, + }) + expect(alias.ok).toBe(true) + const opcode = alias.result.aliases[0].opcode + expect(opcode).toBe(macro.result.macros.at(-1).opcode + 1) + const damaged = encoded.replace( + 'regs[fp + SLOTS.PC] = _operands[0];', + 'regs[fp + SLOTS.PC] = _operands[0] + 1;', + ) + expect(damaged).not.toBe(encoded) + expect( + diagnoseNormalizeAliasedWordcode( + damaged, + diagnoseContainer(damaged).result, + { allowObservedPrefix: true }, + ), + ).toMatchObject({ ok: false, result: null }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) +}) + +test('declines a changed macro body before alias projection', () => { + const encoded = fixture('macro-alias-exceptions', 'encoded.js') + const damaged = encoded.replace( + /(case 61:[\s\S]*?)ReferenceError/, + '$1TypeError', + ) + expect(damaged).not.toBe(encoded) + expect( + diagnoseNormalizeAliasedWordcode( + damaged, + diagnoseContainer(damaged).result, + { allowObservedPrefix: true }, + ), + ).toMatchObject({ ok: false, result: null }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) +}) + +test('keeps anti, specialized, macro, and alias opcode ranges distinct', () => { + const encoded = fixture('eight-option-alias-exceptions', 'encoded.js') + const anti = diagnoseAntiRuntime(encoded, { allowSpecializedTail: true }) + expect(anti.ok).toBe(true) + const specialized = diagnoseSpecializedRuntime(encoded, { + allowAntiPrefix: true, + allowMacroTail: true, + }) + expect(specialized.ok).toBe(true) + const macro = diagnoseMacroRuntime(encoded, { + antiObservation: anti.result, + specializedObservation: specialized.result, + allowAliasTail: true, + }) + expect(macro.ok).toBe(true) + const alias = diagnoseAliasedRuntime(encoded, { + antiObservation: anti.result, + specializedObservation: specialized.result, + macroObservation: macro.result, + }) + expect(alias.ok).toBe(true) + const ranges = [ + anti.result.syntheticCases, + specialized.result.specialized, + macro.result.macros, + alias.result.aliases, + ] + expect(ranges.every((range) => range.length > 0)).toBe(true) + expect(ranges.flat().map(({ opcode }) => opcode)).toEqual( + Array.from({ length: ranges.flat().length }, (_, index) => 61 + index), + ) +}) + +test('stops anti classification before a one-step alias handler', () => { + const source = fixture('anti-alias-exceptions', 'source.js') + const encoded = fixture('anti-alias-exceptions', 'encoded.js') + const record = JSON.parse( + fixture('anti-alias-exceptions', 'qualification.json'), + ) + const anti = diagnoseAntiRuntime(encoded, { allowSpecializedTail: true }) + expect(anti.ok).toBe(true) + expect( + anti.result.syntheticCases.every(({ steps }) => steps.length >= 3), + ).toBe(true) + const aliases = diagnoseAliasedRuntime(encoded, { + antiObservation: anti.result, + }) + expect(aliases.ok).toBe(true) + expect(aliases.result.aliases.length).toBeGreaterThan(0) + expect(aliases.result.aliases[0].opcode).toBe( + anti.result.syntheticCases.at(-1).opcode + 1, + ) + const normalized = diagnoseNormalizeAliasedWordcode( + encoded, + diagnoseContainer(encoded).result, + { allowObservedPrefix: true }, + ) + expect(normalized.ok).toBe(true) + expect(normalized.result.aliasedInstructionCount).toBeGreaterThan(0) + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(observe(encoded)).toEqual(observe(source)) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(record.effectObservation).toMatchObject({ + observed: true, + components: { antiInstrumentation: true, aliasedOpcodes: true }, + }) +}) + +test('retained shuffled runtime moves numeric cases out of opcode order', () => { + const encoded = fixture('shuffled-alias-stack-exceptions', 'encoded.js') + const numericCases = [...encoded.matchAll(/\bcase (\d+):/g)].map((match) => + Number(match[1]), + ) + expect(numericCases.length).toBeGreaterThan(1) + expect(numericCases).not.toEqual( + numericCases.slice().sort((left, right) => left - right), + ) +}) diff --git a/test/vm/jsconfuser-vm/normalize-aliased-wordcode.test.js b/test/vm/jsconfuser-vm/normalize-aliased-wordcode.test.js new file mode 100644 index 00000000..750971bf --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-aliased-wordcode.test.js @@ -0,0 +1,109 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseAliasedRuntime } from '../../../src/vm/jsconfuser-vm/observe-aliased-runtime.js' +import { diagnoseNormalizeAliasedWordcode } from '../../../src/vm/jsconfuser-vm/normalize-aliased-wordcode.js' +import { + diagnoseAliasedWordStream, + diagnoseWordcode, + readWordcode, +} from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test.each([ + 'aliased-runtime-boot', + 'aliased-runtime-closures', + 'aliased-runtime-exceptions', +])('proves, normalizes, and decodes %s', (name) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const qualification = JSON.parse(fixture(name, 'qualification.json')) + const extracted = diagnoseContainer(encoded).result + const originalWords = extracted.roles.words.values.slice() + expect(diagnoseWordcode(extracted)).toMatchObject({ + ok: false, + diagnostic: { code: 'unknown-opcode' }, + }) + const runtime = diagnoseAliasedRuntime(encoded) + expect(runtime.ok).toBe(true) + const stream = diagnoseAliasedWordStream(extracted, runtime.result) + expect(stream.ok).toBe(true) + expect( + stream.result.instructions.some(({ name: op }) => op === 'SYNTHETIC'), + ).toBe(true) + const normalized = diagnoseNormalizeAliasedWordcode(encoded, extracted) + expect(normalized.ok).toBe(true) + expect(normalized.result.originalWordCount).toBe(originalWords.length) + expect(normalized.result.container.roles.words.values).toHaveLength( + originalWords.length, + ) + expect(readWordcode(normalized.result.container)).not.toBeNull() + expect(extracted.roles.words.values).toEqual(originalWords) + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(encoded)).toEqual(observe(source)) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(qualification.effectObservation).toEqual({ + axis: 'serialized-bytecode', + observed: true, + }) + expect(qualification.profile).toBe('aliasedOpcodes') +}) + +test('declines a changed alias handler even when its word stream remains parseable', () => { + const encoded = fixture('aliased-runtime-boot', 'encoded.js') + const damaged = encoded.replace( + 'regs[base + dst] = a * regs[base + _operands[2]];', + 'regs[base + dst] = a * (regs[base + _operands[2]] + 1);', + ) + expect(damaged).not.toBe(encoded) + expect(diagnoseAliasedRuntime(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'alias-runtime' }, + }) + expect( + diagnoseNormalizeAliasedWordcode( + damaged, + diagnoseContainer(damaged).result, + ), + ).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'alias-runtime' }, + }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) +}) diff --git a/test/vm/jsconfuser-vm/normalize-anti-concealed-constants.test.js b/test/vm/jsconfuser-vm/normalize-anti-concealed-constants.test.js new file mode 100644 index 00000000..4a5ec713 --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-anti-concealed-constants.test.js @@ -0,0 +1,77 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseNormalizeAntiWordcode } from '../../../src/vm/jsconfuser-vm/normalize-anti-wordcode.js' +import { diagnoseNormalizeConcealedConstants } from '../../../src/vm/jsconfuser-vm/normalize-concealed-constants.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { validateReferences } from '../../../src/vm/jsconfuser-vm/validate-references.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +const fixtureRoot = new URL( + 'fixtures/concealed-constants-anti-instrumentation/', + import.meta.url, +) +const fixture = (file) => fs.readFileSync(new URL(file, fixtureRoot), 'utf8') + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, atob, Buffer, performance: { now: () => 0 } }, + { timeout: 10000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test('normalizes anti wordcode before keyed constants and preserves finalizer behavior', () => { + const source = fixture('source.js') + const encoded = fixture('encoded.js') + const record = JSON.parse(fixture('qualification.json')) + const container = diagnoseContainer(encoded).result + const before = JSON.stringify(container) + const anti = diagnoseNormalizeAntiWordcode(encoded, container) + expect(anti.ok).toBe(true) + expect(JSON.stringify(container)).toBe(before) + const antiWordcode = readWordcode(anti.result.container) + expect( + antiWordcode.instructions.some((instruction) => + instruction.operands.some( + (operand) => + operand.kind === 'constant-ref' && operand.concealKey !== 0, + ), + ), + ).toBe(true) + expect(validateReferences(anti.result.container, antiWordcode)).toBeNull() + const constants = diagnoseNormalizeConcealedConstants( + encoded, + anti.result.container, + antiWordcode, + ) + expect(constants.ok).toBe(true) + expect( + validateReferences( + constants.result.container, + readWordcode(constants.result.container), + ), + ).not.toBeNull() + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(source)).toEqual(observe(encoded)) + expect(observe(source)).toEqual(observe(decoded.result.output)) + expect(record.effectObservation).toMatchObject({ + observed: true, + components: { concealConstants: true, antiInstrumentation: true }, + }) +}) diff --git a/test/vm/jsconfuser-vm/normalize-anti-routing-combinations.test.js b/test/vm/jsconfuser-vm/normalize-anti-routing-combinations.test.js new file mode 100644 index 00000000..ee0cdd3b --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-anti-routing-combinations.test.js @@ -0,0 +1,146 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseAntiRuntime } from '../../../src/vm/jsconfuser-vm/analyze-anti-runtime.js' +import { diagnoseNormalizeAntiWordcode } from '../../../src/vm/jsconfuser-vm/normalize-anti-wordcode.js' +import { diagnoseNormalizeStringConcealing } from '../../../src/vm/jsconfuser-vm/normalize-string-concealing.js' +import { diagnoseNormalizeCffWordcode } from '../../../src/vm/jsconfuser-vm/normalize-cff-wordcode.js' +import { + diagnoseAntiWordStream, + readWordcode, +} from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { validateReferences } from '../../../src/vm/jsconfuser-vm/validate-references.js' +import { partitionFunctions } from '../../../src/vm/jsconfuser-vm/partition-functions.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, atob, Buffer, performance: { now: () => 0 } }, + { timeout: 30000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +const profiles = [ + ['string-concealing-anti-instrumentation-nested', ['stringConcealing']], + ['cff-anti-instrumentation-exceptions', ['controlFlowFlattening']], + ['dispatcher-anti-instrumentation-switch', ['dispatcher']], + [ + 'string-cff-dispatcher-concealed-constants-anti-instrumentation-exceptions', + [ + 'stringConcealing', + 'controlFlowFlattening', + 'dispatcher', + 'concealConstants', + ], + ], +] + +test.each(profiles)( + 'normalizes %s through anti and the qualified stack', + (name, enabled) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const record = JSON.parse(fixture(name, 'qualification.json')) + const container = diagnoseContainer(encoded).result + const before = JSON.stringify(container) + const anti = diagnoseNormalizeAntiWordcode(encoded, container) + expect(anti.ok).toBe(true) + expect(JSON.stringify(container)).toBe(before) + const wordcode = readWordcode(anti.result.container) + expect(wordcode).not.toBeNull() + const references = validateReferences(anti.result.container, wordcode) + expect(references).not.toBeNull() + if (enabled.length === 1 && enabled[0] === 'stringConcealing') { + expect(wordcode.instructions[0].name).toBe('MAKE_CLOSURE') + expect( + diagnoseNormalizeStringConcealing( + anti.result.container, + wordcode, + references, + ).ok, + ).toBe(true) + } + if (enabled.length === 1 && enabled[0] === 'controlFlowFlattening') { + const functions = partitionFunctions( + anti.result.container, + wordcode, + references, + ) + expect(functions).not.toBeNull() + expect( + diagnoseNormalizeCffWordcode(anti.result.container, wordcode, functions) + .ok, + ).toBe(true) + } + if (enabled.length === 1 && enabled[0] === 'dispatcher') { + expect( + wordcode.instructions.some(({ name: opcode }) => opcode === 'JUMP_REG'), + ).toBe(false) + } + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect( + assertDerivedStateConsistency(output, decoded.result.output).ok, + ).toBe(true) + const sourceValue = observe(source) + expect(sourceValue).toEqual(observe(encoded)) + expect(sourceValue).toEqual(observe(decoded.result.output)) + expect(record.effectObservation.observed).toBe(true) + for (const option of [...enabled, 'antiInstrumentation']) + expect(record.effectObservation.components[option]).toBe(true) + }, +) + +test('declines an encoded dispatcher target without an old PC boundary', () => { + const encoded = fixture( + 'dispatcher-anti-instrumentation-switch', + 'encoded.js', + ) + const container = diagnoseContainer(encoded).result + const runtime = diagnoseAntiRuntime(encoded).result + const stream = diagnoseAntiWordStream(container, runtime).result + const endpoint = stream.instructions.find(({ pc }) => pc === 147) + expect(endpoint).toMatchObject({ name: 'LOAD_INT' }) + const changed = structuredClone(container) + changed.roles.words.values[endpoint.pc + 2] += 1 + expect(diagnoseNormalizeAntiWordcode(encoded, changed)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'dispatcher-target' }, + }) +}) + +test('declines a direct branch into the middle of removed fake seeds', () => { + const encoded = fixture('cff-anti-instrumentation-exceptions', 'encoded.js') + const container = diagnoseContainer(encoded).result + const runtime = diagnoseAntiRuntime(encoded).result + const stream = diagnoseAntiWordStream(container, runtime).result + const jump = stream.instructions.find(({ pc }) => pc === 196) + expect(jump).toMatchObject({ name: 'JUMP' }) + const changed = structuredClone(container) + changed.roles.words.values[jump.pc + 1] = 99 + expect(diagnoseNormalizeAntiWordcode(encoded, changed)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'anti-seed-proof' }, + }) +}) diff --git a/test/vm/jsconfuser-vm/normalize-anti-specialized-combinations.test.js b/test/vm/jsconfuser-vm/normalize-anti-specialized-combinations.test.js new file mode 100644 index 00000000..8d4aabb8 --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-anti-specialized-combinations.test.js @@ -0,0 +1,140 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseAntiRuntime } from '../../../src/vm/jsconfuser-vm/analyze-anti-runtime.js' +import { diagnoseSpecializedRuntime } from '../../../src/vm/jsconfuser-vm/observe-specialized-runtime.js' +import { + diagnoseNormalizeAntiSpecializedWordcode, + diagnoseNormalizeSpecializedWordcode, +} from '../../../src/vm/jsconfuser-vm/normalize-specialized-wordcode.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, atob, Buffer, performance: { now: () => 0 } }, + { timeout: 30000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +const profiles = [ + [ + 'anti-specialized-exceptions', + ['antiInstrumentation', 'specializedOpcodes'], + ], + [ + 'string-cff-dispatcher-concealed-constants-anti-specialized-exceptions', + [ + 'stringConcealing', + 'controlFlowFlattening', + 'dispatcher', + 'concealConstants', + 'antiInstrumentation', + 'specializedOpcodes', + ], + ], +] + +test.each(profiles)( + 'normalizes %s with proved anti and specialized handlers', + (name, options) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const record = JSON.parse(fixture(name, 'qualification.json')) + const container = diagnoseContainer(encoded).result + const before = JSON.stringify(container) + expect(diagnoseAntiRuntime(encoded).ok).toBe(false) + expect(diagnoseSpecializedRuntime(encoded).ok).toBe(false) + const normalized = diagnoseNormalizeAntiSpecializedWordcode( + encoded, + container, + ) + expect(normalized.ok).toBe(true) + expect(JSON.stringify(container)).toBe(before) + expect(normalized.result.specializedInstructionCount).toBeGreaterThan(0) + expect(normalized.result.syntheticInstructionCount).toBeGreaterThan(0) + expect(readWordcode(normalized.result.container)).not.toBeNull() + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect( + assertDerivedStateConsistency(output, decoded.result.output).ok, + ).toBe(true) + const sourceValue = observe(source) + expect(sourceValue).toEqual(observe(encoded)) + expect(sourceValue).toEqual(observe(decoded.result.output)) + expect(record.status).toBe('accepted') + expect(record.effectObservation.observed).toBe(true) + for (const option of options) + expect(record.effectObservation.components[option]).toBe(true) + }, +) + +test('maps old dispatcher PCs after specialized expansion without anti', () => { + const name = + 'string-cff-dispatcher-concealed-constants-specialized-arithmetic' + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const record = JSON.parse(fixture(name, 'qualification.json')) + const container = diagnoseContainer(encoded).result + const before = JSON.stringify(container) + const normalized = diagnoseNormalizeSpecializedWordcode(encoded, container) + expect(normalized.ok).toBe(true) + expect(JSON.stringify(container)).toBe(before) + const wordcode = readWordcode(normalized.result.container) + expect(wordcode).not.toBeNull() + expect( + wordcode.instructions.some(({ name: opcode }) => opcode === 'JUMP_REG'), + ).toBe(false) + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + const sourceValue = observe(source) + expect(sourceValue).toEqual(observe(encoded)) + expect(sourceValue).toEqual(observe(decoded.result.output)) + expect(record.status).toBe('accepted') +}) + +// Opcodes are named by what their handlers do, never by their values: moving a specialized +// handler to another label leaves the words that use it without a handler. +test('declines a word whose specialized handler moved to another label', () => { + const encoded = fixture('anti-specialized-exceptions', 'encoded.js') + const specialized = diagnoseSpecializedRuntime(encoded, { + allowAntiPrefix: true, + }) + expect(specialized.ok).toBe(true) + const opcode = specialized.result.specialized[0].opcode + const changed = encoded.replace(`case ${opcode}:`, `case ${opcode + 500}:`) + expect(changed).not.toBe(encoded) + const container = diagnoseContainer(changed).result + expect( + diagnoseNormalizeAntiSpecializedWordcode(changed, container), + ).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'unknown-opcode' }, + }) + expect(diagnoseStandalone(changed).ok).toBe(false) +}) diff --git a/test/vm/jsconfuser-vm/normalize-anti-wordcode.test.js b/test/vm/jsconfuser-vm/normalize-anti-wordcode.test.js new file mode 100644 index 00000000..fb69240b --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-anti-wordcode.test.js @@ -0,0 +1,159 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseAntiRuntime } from '../../../src/vm/jsconfuser-vm/analyze-anti-runtime.js' +import { diagnoseNormalizeAntiWordcode } from '../../../src/vm/jsconfuser-vm/normalize-anti-wordcode.js' +import { diagnoseNormalizeMacroWordcode } from '../../../src/vm/jsconfuser-vm/normalize-macro-wordcode.js' +import { diagnoseMacroRuntime } from '../../../src/vm/jsconfuser-vm/observe-macro-runtime.js' +import { diagnoseSpecializedRuntime } from '../../../src/vm/jsconfuser-vm/observe-specialized-runtime.js' +import { + diagnoseAntiWordStream, + diagnoseMacroWordStream, + readWordcode, +} from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { + decodeStandalone, + diagnoseStandalone, +} from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test.each([ + 'anti-runtime-boot', + 'anti-runtime-duplicate-step', + 'anti-runtime-finally', + 'anti-runtime-catch-finally', +])( + 'normalizes and decodes %s without changing the source container', + (name) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const extracted = diagnoseContainer(encoded).result + const originalWords = extracted.roles.words.values.slice() + const normalized = diagnoseNormalizeAntiWordcode(encoded, extracted) + expect(normalized.ok).toBe(true) + expect(normalized.result.originalWordCount).toBe(originalWords.length) + expect(normalized.result.container.roles.words.values.length).toBeLessThan( + originalWords.length, + ) + expect(readWordcode(normalized.result.container)).not.toBeNull() + expect(extracted.roles.words.values).toEqual(originalWords) + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + expect( + assertNoVmResidue(parseSuccessfulOutput(decoded.result.output)).ok, + ).toBe(true) + expect( + assertDerivedStateConsistency( + parseSuccessfulOutput(decoded.result.output), + decoded.result.output, + ).ok, + ).toBe(true) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(observe(encoded)).toEqual(observe(source)) + }, +) + +// Anti proves its own fake jumps; a route that expands macro steps first hands them over unchanged. +const fakeJumpRoutes = [ + [ + 'anti', + 'anti-runtime-duplicate-step', + (encoded, container) => diagnoseNormalizeAntiWordcode(encoded, container), + (encoded, extracted, anti) => + diagnoseAntiWordStream(extracted, anti).result, + ], + [ + 'macro', + 'anti-macro-exceptions', + (encoded, container) => diagnoseNormalizeMacroWordcode(encoded, container), + (encoded, extracted, anti) => { + const specialized = diagnoseSpecializedRuntime(encoded, { + allowAntiPrefix: true, + allowMacroTail: true, + }) + const macro = diagnoseMacroRuntime(encoded, { + antiObservation: anti, + specializedObservation: specialized.ok ? specialized.result : null, + }).result + return diagnoseMacroWordStream( + extracted, + macro, + specialized.ok ? specialized.result : null, + anti, + ).result + }, + ], +] + +test.each(fakeJumpRoutes)( + 'declines a changed fake jump on the %s route at anti without a normalized container', + (_, name, normalize, readStream) => { + const encoded = fixture(name, 'encoded.js') + const extracted = diagnoseContainer(encoded).result + const runtime = diagnoseAntiRuntime(encoded, { + allowSpecializedTail: true, + }).result + const stream = readStream(encoded, extracted, runtime) + const [synthetic, definition, jump] = stream.instructions + .filter(({ name }) => name === 'SYNTHETIC') + .map((instruction) => { + const definition = runtime.syntheticCases.find( + ({ opcode }) => opcode === instruction.opcode.value, + ) + const jump = definition.steps.find(({ name }) => + name.startsWith('JUMP_IF'), + ) + return [instruction, definition, jump] + }) + .find(([, , jump]) => jump) + const jumpStored = definition.canonicalToStored[jump.operandStart + 1] + const targetIndex = synthetic.pc + 1 + jumpStored + expect(extracted.roles.words.values[targetIndex]).toBe(synthetic.nextPc) + const tampered = structuredClone(extracted) + const originalWords = extracted.roles.words.values.slice() + // One word past the fall-through: inside the next instruction, never a boundary. + tampered.roles.words.values[targetIndex] += 1 + const normalized = normalize(encoded, tampered) + expect(normalized).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'register-isolation' }, + }) + expect(extracted.roles.words.values).toEqual(originalWords) + + const malformed = encoded.replace( + /^var BYTECODE = \[[^\n]+\];$/m, + `var BYTECODE = ${JSON.stringify(tampered.roles.words.values)};`, + ) + expect(malformed).not.toBe(encoded) + expect(diagnoseStandalone(malformed)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) + expect(decodeStandalone(malformed)).toBeNull() + }, +) diff --git a/test/vm/jsconfuser-vm/normalize-cff-wordcode.test.js b/test/vm/jsconfuser-vm/normalize-cff-wordcode.test.js new file mode 100644 index 00000000..7e0fdbb3 --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-cff-wordcode.test.js @@ -0,0 +1,221 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseNormalizeCffWordcode } from '../../../src/vm/jsconfuser-vm/normalize-cff-wordcode.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { validateReferences } from '../../../src/vm/jsconfuser-vm/validate-references.js' +import { partitionFunctions } from '../../../src/vm/jsconfuser-vm/partition-functions.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function analysesOf(encoded) { + const container = diagnoseContainer(encoded).result + const wordcode = readWordcode(container) + const references = validateReferences(container, wordcode) + const functions = partitionFunctions(container, wordcode, references) + return { container, wordcode, references, functions } +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, Buffer, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test.each([ + 'cff-branches', + 'cff-switch', + 'cff-for-in', + 'cff-try-catch', + 'cff-try-finally', + 'cff-exceptions', +])('normalizes and decodes %s', (name) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const qualification = JSON.parse(fixture(name, 'qualification.json')) + const { container, wordcode, functions } = analysesOf(encoded) + const original = JSON.stringify(container) + const normalized = diagnoseNormalizeCffWordcode( + container, + wordcode, + functions, + ) + expect(normalized.ok).toBe(true) + expect(JSON.stringify(container)).toBe(original) + expect(normalized.result.functionCount).toBeGreaterThan(0) + expect(normalized.result.normalizedWordCount).toBeLessThan( + normalized.result.originalWordCount, + ) + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(encoded)).toEqual(observe(source)) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(qualification.profile).toBe('controlFlowFlattening') + expect(qualification.effectObservation).toEqual({ + axis: 'serialized-bytecode', + observed: true, + }) +}) + +// CFF reorders every block. The decoder keeps every jump the stream does not prove a fallthrough +// and places only a conditional's proved fallthrough (the encoder jumps over a transition to it) +// after its test; the backend reads loops, breaks and tries from the graph. The CFF decode reads +// as the plain decode does (registers and dead blocks aside). +test('restores the order of loop and try exits that flattening reorders', () => { + const source = fixture('cff-loop-return', 'source.js') + const read = (file) => { + const decoded = diagnoseStandalone(fixture('cff-loop-return', file)) + expect(decoded.ok, JSON.stringify(decoded.diagnostic)).toBe(true) + expect(observe(decoded.result.output)).toEqual(observe(source)) + return decoded.result.output + .replace(/__recovered_r_\d+_\d+/g, 'R') + .replace(/let [^;]*;/g, 'let;') + .replace(/\n *if \(false\) \{\n(?: {4,}.*\n)*? *\}/g, '') + } + const flattened = read('encoded.js') + expect(flattened).toBe(read('plain.encoded.js')) +}) + +// The pass removes what CFF added (the state loop, its transitions and seeds, the fake blocks) and +// nothing else: unreachable code the compiler emitted, source dead code included, goes to the +// backend. So apart from jumps, the normalized stream holds the plain compile's instructions. +test.each(['normalize-cff-wordcode/dead-code', 'cff-loop-return'])( + 'removes only its own fake blocks from %s', + (name) => { + const opcodes = ({ instructions }) => { + const counts = {} + for (const { name: opcode } of instructions) + if (opcode !== 'JUMP') counts[opcode] = (counts[opcode] ?? 0) + 1 + return counts + } + const { container, wordcode, functions } = analysesOf( + fixture(name, 'encoded.js'), + ) + const normalized = diagnoseNormalizeCffWordcode( + container, + wordcode, + functions, + ) + expect(normalized.ok, JSON.stringify(normalized.diagnostic)).toBe(true) + expect(opcodes(normalized.result.wordcode)).toEqual( + opcodes(analysesOf(fixture(name, 'plain.encoded.js')).wordcode), + ) + const decoded = diagnoseStandalone(fixture(name, 'encoded.js')) + expect(decoded.ok, JSON.stringify(decoded.diagnostic)).toBe(true) + expect(observe(decoded.result.output)).toEqual( + observe(fixture(name, 'source.js')), + ) + }, +) + +// Every non-arrow function opens with LOAD_THIS, as the state-loop template does, so a plain loop +// on two fresh locals spells the state-loop prefix; it fails the arm grammar and is not CFF. +test('reads a plain loop sharing the state-loop prefix as no CFF', () => { + const source = fixture('loop-sharing-cff-prefix', 'source.js') + const encoded = fixture('loop-sharing-cff-prefix', 'encoded.js') + const { container, wordcode, functions } = analysesOf(encoded) + const loop = wordcode.instructions.filter( + (item) => + item.pc >= functions.functions[1].startPc && + item.pc < functions.functions[1].endPc, + ) + expect(loop.slice(0, 8).map((item) => item.name)).toEqual([ + 'LOAD_THIS', + 'LOAD_CONST', + 'MOVE', + 'LOAD_CONST', + 'MOVE', + 'LOAD_CONST', + 'NEQ', + 'JUMP_IF_FALSE', + ]) + expect( + diagnoseNormalizeCffWordcode(container, wordcode, functions), + ).toMatchObject({ ok: false, diagnostic: { code: 'cff-absent' } }) + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok, JSON.stringify(decoded.diagnostic)).toBe(true) + expect(observe(decoded.result.output)).toEqual(observe(source)) +}) + +// The synthetic exact word-PC example from the CFF specification, on a private +// container cloned from the reference fixture. Its omitted prefix starts have no first +// map entries, so a branch into one is refused rather than retargeted. +test('normalizes the specification scaffold and refuses targets in its omitted prefix', () => { + const reference = diagnoseContainer(fixture('reference', 'encoded.js')).result + const words = [ + 4, 0, 0, 1, 0, 0, 5, 2, 1, 0, 1, 1, 0, 5, 3, 1, 0, 1, 2, 0, 27, 4, 2, 1, 40, + 4, 65, 0, 1, 0, 0, 5, 3, 1, 26, 4, 2, 3, 40, 4, 43, 39, 76, 0, 1, 0, 0, 11, + 3, 3, 1, 5, 3, 3, 26, 4, 2, 3, 40, 4, 63, 39, 71, 39, 16, 0, 5, 3, 0, 45, 5, + 1, 5, 42, 45, 5, 1, 5, 7, 1, 1, 10, 11, 2, 2, 1, 39, 16, + ] + const normalize = (values) => { + const container = structuredClone(reference) + container.roles.words.values = values + container.roles.pool.values = [10, 0, 60, undefined] + container.roles.scalars.MAIN_REG_COUNT.value = 6 + const wordcode = readWordcode(container) + const references = validateReferences(container, wordcode) + const functions = partitionFunctions(container, wordcode, references) + const supplied = JSON.stringify([ + container, + wordcode, + references, + functions, + ]) + const result = diagnoseNormalizeCffWordcode(container, wordcode, functions) + expect(JSON.stringify([container, wordcode, references, functions])).toBe( + supplied, + ) + return result + } + expect(words).toHaveLength(88) + // A's transition to B proves no fallthrough, so it stays a jump; the end body is no fake + // block, so it stays too, for the backend to read as unreachable. + expect(normalize(words).result.container.roles.words.values).toEqual([ + 1, 5, 7, 39, 11, 0, 5, 3, 0, 45, 5, 1, 5, 42, 45, 5, + ]) + + const intoPrefix = [...words] + intoPrefix.splice(71, 3, 41, 5, 2) // JUMP_IF_TRUE r5,2 replaces LOAD_INT r5,42 + expect(intoPrefix).toHaveLength(88) + expect(normalize(intoPrefix)).toMatchObject({ + ok: false, + result: null, + diagnostic: { + code: 'cff-shape', + message: 'Target 2 is not an instruction boundary', + }, + }) + + const unknownState = [...words] + unknownState[81] = 11 + expect(normalize(unknownState)).toMatchObject({ + ok: false, + result: null, + diagnostic: { + code: 'cff-shape', + message: 'Relative transition reaches unknown state 21', + }, + }) +}) diff --git a/test/vm/jsconfuser-vm/normalize-dispatcher-wordcode.test.js b/test/vm/jsconfuser-vm/normalize-dispatcher-wordcode.test.js new file mode 100644 index 00000000..3ee51bf7 --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-dispatcher-wordcode.test.js @@ -0,0 +1,238 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseDispatcherWordcode } from '../../../src/vm/jsconfuser-vm/observe-dispatcher-wordcode.js' +import { diagnoseNormalizeDispatcherWordcode } from '../../../src/vm/jsconfuser-vm/normalize-dispatcher-wordcode.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { validateReferences } from '../../../src/vm/jsconfuser-vm/validate-references.js' +import { partitionFunctions } from '../../../src/vm/jsconfuser-vm/partition-functions.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function analysesOf(encoded) { + const container = diagnoseContainer(encoded).result + const wordcode = readWordcode(container) + const references = validateReferences(container, wordcode) + const functions = partitionFunctions(container, wordcode, references) + return { container, wordcode, references, functions } +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, Buffer, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +function withWords(encoded, words) { + const original = encoded.match(/var BYTECODE = \[[\s\S]*?\];/)?.[0] + expect(original).toBeDefined() + return encoded.replace(original, `var BYTECODE = [${words.join(',')}];`) +} + +test.each([ + 'dispatcher-branches', + 'dispatcher-array-result', + 'dispatcher-split-key', + 'dispatcher-object-result', + 'dispatcher-switch', + 'dispatcher-finally', + 'dispatcher-exceptions', +])('proves, normalizes, and decodes %s', (name) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const qualification = JSON.parse(fixture(name, 'qualification.json')) + const { container, wordcode, functions } = analysesOf(encoded) + const observation = diagnoseDispatcherWordcode(container, wordcode, functions) + expect(observation.ok).toBe(true) + expect(observation.result.plans.length).toBeGreaterThan(0) + const original = JSON.stringify(container) + const normalized = diagnoseNormalizeDispatcherWordcode( + container, + wordcode, + functions, + ) + expect(normalized.ok).toBe(true) + expect(JSON.stringify(container)).toBe(original) + expect(normalized.result.routeCount).toBeGreaterThan(0) + expect(normalized.result.normalizedWordCount).toBeLessThan( + normalized.result.originalWordCount, + ) + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(encoded)).toEqual(observe(source)) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(qualification.profile).toBe('dispatcher') + expect(qualification.effectObservation).toEqual({ + axis: 'serialized-bytecode', + observed: true, + }) +}) + +test('declines a changed decode-closure global before normalization', () => { + const encoded = fixture('dispatcher-branches', 'encoded.js') + const { container, wordcode, functions } = analysesOf(encoded) + const observed = diagnoseDispatcherWordcode(container, wordcode, functions) + const closure = functions.functions.find( + (item) => item.id === observed.result.plans[0].closureId, + ) + const globalLoad = closure.instructionPcs + .map((pc) => wordcode.instructions.find((item) => item.pc === pc)) + .find((item) => item.name === 'LOAD_GLOBAL') + const words = container.roles.words.values.slice() + words[globalLoad.pc + 2] = container.roles.pool.values.indexOf('window') + const damaged = withWords(encoded, words) + const changed = analysesOf(damaged) + expect( + diagnoseDispatcherWordcode( + changed.container, + changed.wordcode, + changed.functions, + ), + ).toMatchObject({ + ok: false, + diagnostic: { code: 'dispatcher-closure' }, + }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'unresolved-indirect-jump' }, + }) +}) + +test('declines a site key that decodes outside instruction boundaries', () => { + const encoded = fixture('dispatcher-branches', 'encoded.js') + const { container, wordcode, functions } = analysesOf(encoded) + const observed = diagnoseDispatcherWordcode(container, wordcode, functions) + const route = observed.result.plans[0].routes[0] + const keyLoadPc = route.oldPcs.at(-2) + const words = container.roles.words.values.slice() + words[keyLoadPc + 2] = (words[keyLoadPc + 2] + 1) >>> 0 + const damaged = withWords(encoded, words) + const changed = analysesOf(damaged) + expect( + diagnoseDispatcherWordcode( + changed.container, + changed.wordcode, + changed.functions, + ), + ).toMatchObject({ + ok: false, + diagnostic: { code: 'dispatcher-target' }, + }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'unresolved-indirect-jump' }, + }) +}) + +test.each(['state', 'key', 'selector temporary'])( + 'declines a payload use of erased dispatcher %s through source input', + (role) => { + const encoded = fixture('dispatcher-branches', 'encoded.js') + const { container, wordcode, functions } = analysesOf(encoded) + const plan = diagnoseDispatcherWordcode(container, wordcode, functions) + .result.plans[0] + const jump = wordcode.instructions.find( + (item) => item.pc === plan.dispatcherPcs.at(-1), + ) + const publish = wordcode.instructions.find( + (item) => item.name === 'SET_PROP', + ) + const route = plan.routes.find((item) => item.name !== 'JUMP') + expect(route).toBeDefined() + const keyLoad = wordcode.instructions.find( + (item) => item.pc === route.oldPcs.at(-2), + ) + const selector = wordcode.instructions.find( + (item) => route.oldPcs.includes(item.pc) && item.name === 'SUB', + ) + const register = + role === 'state' + ? jump.words[1] + : role === 'key' + ? keyLoad.words[1] + : selector.words[1] + const words = container.roles.words.values.slice() + words[publish.pc + 3] = register + const changedSource = withWords(encoded, words) + expect(typeof observe(changedSource)).toBe('number') + const changed = analysesOf(changedSource) + expect( + diagnoseDispatcherWordcode( + changed.container, + changed.wordcode, + changed.functions, + ), + ).toMatchObject({ + ok: false, + diagnostic: { + code: 'dispatcher-shape', + message: expect.stringContaining('Dispatcher register escapes'), + }, + }) + expect(diagnoseStandalone(changedSource)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'unresolved-indirect-jump' }, + }) + }, +) + +test('declines an erased state used by a retained dispatcher condition', () => { + const encoded = fixture('dispatcher-branches', 'encoded.js') + const { container, wordcode, functions } = analysesOf(encoded) + const plan = diagnoseDispatcherWordcode(container, wordcode, functions).result + .plans[0] + const route = plan.routes.find((item) => item.condition !== undefined) + const invert = wordcode.instructions.find( + (item) => item.pc === route.oldPcs[0], + ) + const jump = wordcode.instructions.find( + (item) => item.pc === plan.dispatcherPcs.at(-1), + ) + expect(invert.name).toBe('UNARY_NOT') + const words = container.roles.words.values.slice() + words[invert.pc + 2] = jump.words[1] + const changedSource = withWords(encoded, words) + const changed = analysesOf(changedSource) + expect( + diagnoseDispatcherWordcode( + changed.container, + changed.wordcode, + changed.functions, + ), + ).toMatchObject({ + ok: false, + diagnostic: { + code: 'dispatcher-shape', + message: 'Dispatcher condition uses erased state', + }, + }) + expect(diagnoseStandalone(changedSource)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'unresolved-indirect-jump' }, + }) +}) diff --git a/test/vm/jsconfuser-vm/normalize-macro-combinations.test.js b/test/vm/jsconfuser-vm/normalize-macro-combinations.test.js new file mode 100644 index 00000000..f6c3d9f8 --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-macro-combinations.test.js @@ -0,0 +1,163 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseAntiRuntime } from '../../../src/vm/jsconfuser-vm/analyze-anti-runtime.js' +import { diagnoseSpecializedRuntime } from '../../../src/vm/jsconfuser-vm/observe-specialized-runtime.js' +import { diagnoseMacroRuntime } from '../../../src/vm/jsconfuser-vm/observe-macro-runtime.js' +import { diagnoseNormalizeMacroWordcode } from '../../../src/vm/jsconfuser-vm/normalize-macro-wordcode.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + diagnoseMacroWordStream, + readWordcode, +} from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, atob, btoa, performance: { now: () => 0 } }, + { timeout: 30000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +const sevenOptions = [ + 'stringConcealing', + 'controlFlowFlattening', + 'dispatcher', + 'concealConstants', + 'antiInstrumentation', + 'specializedOpcodes', + 'macroOpcodes', +] + +const samples = [ + ['anti-macro-exceptions', ['antiInstrumentation', 'macroOpcodes']], + ['specialized-macro-nested-strings', ['specializedOpcodes', 'macroOpcodes']], + ['macro-seven-option-exceptions', sevenOptions], + ['macro-seven-option-repeated-arrays', sevenOptions], + ['macro-six-option-no-anti-exceptions', null], +] + +test.each(samples)( + 'normalizes and decodes %s', + (name, effectOptions) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const record = JSON.parse(fixture(name, 'qualification.json')) + const container = diagnoseContainer(encoded).result + const before = JSON.stringify(container) + const normalized = diagnoseNormalizeMacroWordcode(encoded, container) + expect(normalized.ok).toBe(true) + expect(normalized.result.macroInstructionCount).toBeGreaterThan(0) + expect(JSON.stringify(container)).toBe(before) + expect(readWordcode(normalized.result.container)).not.toBeNull() + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect( + assertDerivedStateConsistency(output, decoded.result.output).ok, + ).toBe(true) + const expected = observe(source) + expect(observe(encoded)).toEqual(expected) + expect(observe(decoded.result.output)).toEqual(expected) + expect(record.status).toBe('accepted') + if (effectOptions) { + expect(record.effectObservation.observed).toBe(true) + for (const option of effectOptions) + expect(record.effectObservation.components[option]).toBe(true) + } + }, +) + +test('declines a changed macro case in the mixed runtime', () => { + const encoded = fixture('macro-seven-option-exceptions', 'encoded.js') + const anti = diagnoseAntiRuntime(encoded, { allowSpecializedTail: true }) + expect(anti.ok).toBe(true) + const specialized = diagnoseSpecializedRuntime(encoded, { + allowAntiPrefix: true, + allowMacroTail: true, + }) + expect(specialized.ok).toBe(true) + const macro = diagnoseMacroRuntime(encoded, { + antiObservation: anti.result, + specializedObservation: specialized.result, + }) + expect(macro.ok).toBe(true) + const opcode = macro.result.macros[0].opcode + const damaged = encoded.replace(`case ${opcode}:`, `case ${opcode + 500}:`) + expect(damaged).not.toBe(encoded) + expect( + diagnoseNormalizeMacroWordcode(damaged, diagnoseContainer(damaged).result), + ).toMatchObject({ ok: false, result: null }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) +}) + +test('declines a changed specialized prefix before macro expansion', () => { + const encoded = fixture('macro-seven-option-exceptions', 'encoded.js') + const specialized = diagnoseSpecializedRuntime(encoded, { + allowAntiPrefix: true, + allowMacroTail: true, + }) + expect(specialized.ok).toBe(true) + const opcode = specialized.result.specialized[0].opcode + const damaged = encoded.replace(`case ${opcode}:`, `case ${opcode + 500}:`) + expect(damaged).not.toBe(encoded) + expect( + diagnoseNormalizeMacroWordcode(damaged, diagnoseContainer(damaged).result), + ).toMatchObject({ ok: false, result: null }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) +}) + +test('keeps variable-width array builders inside proved mixed macros', () => { + const encoded = fixture('macro-seven-option-repeated-arrays', 'encoded.js') + const anti = diagnoseAntiRuntime(encoded, { allowSpecializedTail: true }) + expect(anti.ok).toBe(true) + const specialized = diagnoseSpecializedRuntime(encoded, { + allowAntiPrefix: true, + allowMacroTail: true, + }) + expect(specialized.ok).toBe(true) + const macro = diagnoseMacroRuntime(encoded, { + antiObservation: anti.result, + specializedObservation: specialized.result, + }) + expect(macro.ok).toBe(true) + const stream = diagnoseMacroWordStream( + diagnoseContainer(encoded).result, + macro.result, + specialized.result, + anti.result, + ) + expect(stream.ok).toBe(true) + expect( + stream.result.instructions.some( + (instruction) => + instruction.name === 'MACRO' && + instruction.steps.some(({ name }) => name === 'BUILD_ARRAY'), + ), + ).toBe(true) +}) diff --git a/test/vm/jsconfuser-vm/normalize-macro-wordcode.test.js b/test/vm/jsconfuser-vm/normalize-macro-wordcode.test.js new file mode 100644 index 00000000..bb2eea6f --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-macro-wordcode.test.js @@ -0,0 +1,181 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseMacroRuntime } from '../../../src/vm/jsconfuser-vm/observe-macro-runtime.js' +import { diagnoseNormalizeMacroWordcode } from '../../../src/vm/jsconfuser-vm/normalize-macro-wordcode.js' +import { + diagnoseMacroWordStream, + diagnoseWordcode, + readWordcode, +} from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test.each([ + 'macro-runtime-boot', + 'macro-runtime-branches', + 'macro-runtime-closures', + 'macro-runtime-exceptions', + 'macro-runtime-arrays', +])('proves, expands, and decodes %s', (name) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const qualification = JSON.parse(fixture(name, 'qualification.json')) + const extracted = diagnoseContainer(encoded) + expect(extracted.ok).toBe(true) + expect(diagnoseWordcode(extracted.result)).toMatchObject({ + ok: false, + diagnostic: { code: 'unknown-opcode' }, + }) + const runtime = diagnoseMacroRuntime(encoded) + expect(runtime.ok).toBe(true) + const stream = diagnoseMacroWordStream(extracted.result, runtime.result) + expect(stream.ok).toBe(true) + if (name === 'macro-runtime-arrays') { + expect( + stream.result.instructions.some( + (instruction) => + instruction.name === 'MACRO' && + instruction.steps.some((step) => step.name === 'BUILD_ARRAY'), + ), + ).toBe(true) + } + const normalized = diagnoseNormalizeMacroWordcode(encoded, extracted.result) + expect(normalized.ok).toBe(true) + expect(normalized.result.expandedWordCount).toBeGreaterThan( + normalized.result.originalWordCount, + ) + expect(readWordcode(normalized.result.container)).not.toBeNull() + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(encoded)).toEqual(observe(source)) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(qualification.profile).toBe('macroOpcodes') + expect(qualification.effectObservation).toEqual({ + axis: 'serialized-bytecode', + observed: true, + }) +}) + +test('declines a changed macro body before word normalization', () => { + const encoded = fixture('macro-runtime-boot', 'encoded.js') + const damaged = encoded.replace( + /(?<=\/\/ LOAD_CONST,LOAD_CONST \(macro\)[\s\S]*?)regs\[base \+ dst\] = this\._constant\(\);/, + 'regs[base + dst] = this._constant() + 1;', + ) + expect(damaged).not.toBe(encoded) + expect(diagnoseMacroRuntime(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'macro-runtime' }, + }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) +}) + +test('declines a branch into the middle of an original macro instruction', () => { + const encoded = fixture('macro-runtime-branches', 'encoded.js') + const extracted = diagnoseContainer(encoded).result + const words = extracted.roles.words.values.slice() + const macro = diagnoseMacroWordStream( + extracted, + diagnoseMacroRuntime(encoded).result, + ).result.instructions.find(({ name }) => name === 'MACRO') + const branchPc = diagnoseMacroWordStream( + extracted, + diagnoseMacroRuntime(encoded).result, + ).result.instructions.find(({ name }) => name === 'JUMP_IF_FALSE').pc + words[branchPc + 2] = macro.pc + 1 + const damaged = structuredClone(extracted) + damaged.roles.words.values = words + expect(diagnoseNormalizeMacroWordcode(encoded, damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'macro-target-boundary' }, + }) + const malformed = encoded.replace( + /^var BYTECODE = \[[^\n]+\];$/m, + `var BYTECODE = ${JSON.stringify(words)};`, + ) + expect(malformed).not.toBe(encoded) + expect(diagnoseStandalone(malformed)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) +}) + +test('declines a source macro whose counted array payload exceeds the stream', () => { + const encoded = fixture('macro-runtime-arrays', 'encoded.js') + const container = diagnoseContainer(encoded).result + const runtime = diagnoseMacroRuntime(encoded).result + const stream = diagnoseMacroWordStream(container, runtime).result + const macro = stream.instructions.find( + (instruction) => + instruction.name === 'MACRO' && + instruction.steps.some(({ name }) => name === 'BUILD_ARRAY'), + ) + expect(macro).toBeDefined() + const stepIndex = macro.steps.findIndex(({ name }) => name === 'BUILD_ARRAY') + const precedingWords = macro.steps + .slice(0, stepIndex) + .reduce((total, step) => total + step.words.length - 1, 0) + const countPc = macro.pc + 2 + precedingWords + const words = container.roles.words.values.slice() + words[countPc] = 0xffffffff + const damaged = encoded.replace( + /^var BYTECODE = \[[^\n]+\];$/m, + `var BYTECODE = ${JSON.stringify(words)};`, + ) + expect(damaged).not.toBe(encoded) + const admitted = diagnoseContainer(damaged) + const observed = diagnoseMacroRuntime(damaged) + expect(admitted.ok).toBe(true) + expect(observed.ok).toBe(true) + expect( + diagnoseMacroWordStream(admitted.result, observed.result), + ).toMatchObject({ + ok: false, + result: null, + diagnostic: { + code: 'truncated-instruction', + message: expect.stringContaining('BUILD_ARRAY'), + }, + }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) +}) diff --git a/test/vm/jsconfuser-vm/normalize-self-modifying-wordcode.test.js b/test/vm/jsconfuser-vm/normalize-self-modifying-wordcode.test.js new file mode 100644 index 00000000..c7eabfc2 --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-self-modifying-wordcode.test.js @@ -0,0 +1,138 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseNormalizeSelfModifyingWordcode } from '../../../src/vm/jsconfuser-vm/normalize-self-modifying-wordcode.js' +import { diagnoseSelfModifyingRegions } from '../../../src/vm/jsconfuser-vm/observe-self-modifying.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { + decodeStandalone, + diagnoseStandalone, +} from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, performance: { now: () => 0 }, Buffer, atob }, + { timeout: 1000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +function replaceWords(encoded, words) { + const changed = encoded.replace( + /^var BYTECODE = \[[^\n]+\];$/m, + `var BYTECODE = ${JSON.stringify(words)};`, + ) + expect(changed).not.toBe(encoded) + return changed +} + +// selfModifying over dispatcher, string concealing, CFF and anti: the PATCH stage removes only its +// own words (headers, decoys, decoy fillers, storage) and hands the inner opcodes on with its map. +const layers = JSON.parse( + fixture('self-modifying-layers', 'generation.json'), +).files + +test.each(Object.keys(layers))( + 'decodes layered self-modifying %s with source behaviour', + (file) => { + const source = fixture( + 'self-modifying-layers', + layers[file].source ?? 'source.js', + ) + const encoded = fixture('self-modifying-layers', file) + expect(diagnoseContainer(encoded).ok).toBe(true) + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok, JSON.stringify(decoded.diagnostic)).toBe(true) + const ast = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(ast).ok).toBe(true) + expect(assertDerivedStateConsistency(ast, decoded.result.output).ok).toBe( + true, + ) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(observe(encoded)).toEqual(observe(source)) + }, +) + +test.each([ + 'self-modifying-boot', + 'self-modifying-closures', + 'self-modifying-finally', +])('normalizes and decodes %s with no VM residue', (name) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const analyses = diagnoseContainer(encoded).result + const originalWords = analyses.roles.words.values.slice() + const normalized = diagnoseNormalizeSelfModifyingWordcode(analyses) + expect(normalized.ok).toBe(true) + expect(normalized.result.wordCount).toBeLessThan(originalWords.length) + expect(readWordcode(normalized.result.container)).not.toBeNull() + expect(analyses.roles.words.values).toEqual(originalWords) + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const ast = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(ast).ok).toBe(true) + expect(assertDerivedStateConsistency(ast, decoded.result.output).ok).toBe( + true, + ) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(observe(encoded)).toEqual(observe(source)) +}) + +// A finalizer's continuation register is written only by label loads, so its literal is an +// address the stage relocates; one pointing into PATCH storage names no code and declines. +test('declines a finally continuation redirected into storage', () => { + const encoded = fixture('self-modifying-finally', 'encoded.js') + const analyses = diagnoseContainer(encoded).result + const originalWords = analyses.roles.words.values.slice() + const observation = diagnoseSelfModifyingRegions(analyses).result + const setup = observation.instructions.find( + (item) => item.name === 'FINALLY_SETUP', + ) + const load = observation.instructions.find( + (item) => item.name === 'LOAD_INT' && item.words[1] === setup.words[2], + ) + expect(load).toBeDefined() + const target = observation.patches[0].sourceStart + const changed = structuredClone(analyses) + const words = changed.roles.words.values + const at = load.pc + 2 + const region = observation.patches.find( + ({ destPc, width }) => at >= destPc && at < destPc + width, + ) + if (region) { + // Change the plaintext through its ciphertext word: c' = c ^ p ^ p'. + const source = region.sourceStart + (at - region.destPc) + words[source] = + (words[source] ^ observation.patchedWords[at] ^ target) >>> 0 + } else words[at] = target + expect(diagnoseNormalizeSelfModifyingWordcode(changed)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'self-modifying-target' }, + }) + expect(analyses.roles.words.values).toEqual(originalWords) + + const malformed = replaceWords(encoded, changed.roles.words.values) + expect(diagnoseStandalone(malformed)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) + expect(decodeStandalone(malformed)).toBeNull() +}) diff --git a/test/vm/jsconfuser-vm/normalize-specialized-wordcode.test.js b/test/vm/jsconfuser-vm/normalize-specialized-wordcode.test.js new file mode 100644 index 00000000..02093c73 --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-specialized-wordcode.test.js @@ -0,0 +1,98 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseSpecializedRuntime } from '../../../src/vm/jsconfuser-vm/observe-specialized-runtime.js' +import { diagnoseNormalizeSpecializedWordcode } from '../../../src/vm/jsconfuser-vm/normalize-specialized-wordcode.js' +import { + diagnoseSpecializedWordStream, + diagnoseWordcode, + readWordcode, +} from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test.each([ + 'specialized-runtime-boot', + 'specialized-runtime-branches', + 'specialized-runtime-closures', + 'specialized-runtime-exceptions', +])('proves, expands, and decodes %s', (name) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const qualification = JSON.parse(fixture(name, 'qualification.json')) + const extracted = diagnoseContainer(encoded) + expect(extracted.ok).toBe(true) + expect(diagnoseWordcode(extracted.result)).toMatchObject({ + ok: false, + diagnostic: { code: 'unknown-opcode' }, + }) + const runtime = diagnoseSpecializedRuntime(encoded) + expect(runtime.ok).toBe(true) + const stream = diagnoseSpecializedWordStream(extracted.result, runtime.result) + expect(stream.ok).toBe(true) + const normalized = diagnoseNormalizeSpecializedWordcode( + encoded, + extracted.result, + ) + expect(normalized.ok).toBe(true) + expect(normalized.result.expandedWordCount).toBeGreaterThan( + normalized.result.originalWordCount, + ) + expect(readWordcode(normalized.result.container)).not.toBeNull() + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(encoded)).toEqual(observe(source)) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(qualification.profile).toBe('specializedOpcodes') + expect(qualification.effectObservation).toEqual({ + axis: 'serialized-bytecode', + observed: true, + }) +}) + +test('declines a changed specialized handler before word expansion', () => { + const encoded = fixture('specialized-runtime-boot', 'encoded.js') + const damaged = encoded.replace( + /(?<=\/\/ MUL_[^\n]+\(specialized\)[\s\S]*?)a \* regs\[base \+ 5\]/, + 'a * (regs[base + 5] + 1)', + ) + expect(damaged).not.toBe(encoded) + expect(diagnoseSpecializedRuntime(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'specialized-runtime' }, + }) + expect(diagnoseStandalone(damaged)).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'wordcode-declined' }, + }) +}) diff --git a/test/vm/jsconfuser-vm/normalize-string-concealing-combinations.test.js b/test/vm/jsconfuser-vm/normalize-string-concealing-combinations.test.js new file mode 100644 index 00000000..0399bbef --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-string-concealing-combinations.test.js @@ -0,0 +1,298 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import traverse from '@babel/traverse' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { validateReferences } from '../../../src/vm/jsconfuser-vm/validate-references.js' +import { + diagnoseFunctionPartition, + partitionFunctions, +} from '../../../src/vm/jsconfuser-vm/partition-functions.js' +import { diagnoseNormalizeStringConcealing } from '../../../src/vm/jsconfuser-vm/normalize-string-concealing.js' +import { diagnoseNormalizeConcealedConstants } from '../../../src/vm/jsconfuser-vm/normalize-concealed-constants.js' +import { diagnoseNormalizeCffWordcode } from '../../../src/vm/jsconfuser-vm/normalize-cff-wordcode.js' +import { diagnoseNormalizeDispatcherWordcode } from '../../../src/vm/jsconfuser-vm/normalize-dispatcher-wordcode.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, atob, Buffer, performance: { now: () => 0 } }, + { timeout: 10000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +function withWords(encoded, words) { + const original = encoded.match(/var BYTECODE = \[[\s\S]*?\];/)?.[0] + expect(original).toBeDefined() + return encoded.replace(original, `var BYTECODE = [${words.join(',')}];`) +} + +function stringLiterals(ast) { + const values = new Set() + traverse(ast, { + StringLiteral(path) { + values.add(path.node.value) + }, + }) + return values +} + +// A shared helper is a decode closure created in several functions, which ordinary partitionFunctions refuses. +// The main-branch cells flatten main, so CFF hides the string prologue from the string stage; the +// finally-continuation cell's site opens a block entered only through the finalizer's JUMP_REG. +test.each([ + ['string-concealing-cff', true], + ['string-concealing-dispatcher', true], + ['string-concealing-nested-cff', true], + ['string-concealing-cff-dispatcher-exceptions', true], + ['string-concealing-cff-dispatcher-switch', true], + ['string-cff-dispatcher-anti-instrumentation-main-branch', true], + ['string-concealing-cff-main-branch', false], + ['string-concealing-cff-finally-continuation', false], +])('restores stacked string helpers in %s', (name, sharedHelper) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const record = JSON.parse(fixture(name, 'qualification.json')) + const container = diagnoseContainer(encoded).result + const wordcode = readWordcode(container) + const references = validateReferences(container, wordcode) + if (sharedHelper) + expect(partitionFunctions(container, wordcode, references)).toBeNull() + expect( + diagnoseNormalizeStringConcealing(container, wordcode, references).ok, + ).toBe(false) + const result = diagnoseStandalone(encoded) + expect(result.ok).toBe(true) + const output = parseSuccessfulOutput(result.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + // Behaviour alone does not prove recovery: the helpers rebuild every string at run time. + const restored = stringLiterals(output) + for (const value of stringLiterals(parseSuccessfulOutput(source))) + expect(restored.has(value), value).toBe(true) + expect(assertDerivedStateConsistency(output, result.result.output).ok).toBe( + true, + ) + expect(observe(source)).toEqual(observe(encoded)) + expect(observe(source)).toEqual(observe(result.result.output)) + expect(record.effectObservation).toMatchObject({ + axis: 'serialized-bytecode-components', + observed: true, + components: { stringConcealing: true }, + }) +}) + +test.each(['string-concealing-cff', 'string-concealing-dispatcher'])( + 'declines an altered decoded helper in %s', + (name) => { + const encoded = fixture(name, 'encoded.js') + const container = diagnoseContainer(encoded).result + const wordcode = readWordcode(container) + const decodeStart = wordcode.instructions.find( + (item) => + item.name === 'MAKE_CLOSURE' && item.functionMeta.paramCount === 4, + ).words[2] + const helper = wordcode.instructions.find( + (item) => item.pc > decodeStart && item.name === 'USHR', + ) + expect(helper).toBeDefined() + const changed = container.roles.words.values.slice() + changed[helper.pc] = container.roles.op.values.BXOR + expect(diagnoseStandalone(withWords(encoded, changed))).toMatchObject({ + ok: false, + result: null, + }) + }, +) + +test('restores a finalizer when CFF and dispatcher are stacked', () => { + const name = 'cff-dispatcher-try-finally' + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const record = JSON.parse(fixture(name, 'qualification.json')) + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(source)).toEqual(observe(encoded)) + expect(observe(source)).toEqual(observe(decoded.result.output)) + expect(record.effectObservation).toMatchObject({ + observed: true, + components: { controlFlowFlattening: true, dispatcher: true }, + }) +}) + +test('restores the signed Weyl increment after constant concealment', () => { + const name = 'string-concealing-concealed-constants' + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const record = JSON.parse(fixture(name, 'qualification.json')) + const container = diagnoseContainer(encoded).result + const wordcode = readWordcode(container) + const normalizedConstants = diagnoseNormalizeConcealedConstants( + encoded, + container, + wordcode, + ) + expect(normalizedConstants.ok).toBe(true) + const plain = normalizedConstants.result.container + const plainWordcode = readWordcode(plain) + const references = validateReferences(plain, plainWordcode) + expect(plain.roles.pool.values).toContain(-1640531527) + expect( + diagnoseNormalizeStringConcealing(plain, plainWordcode, references).ok, + ).toBe(true) + const changed = structuredClone(plain) + changed.roles.pool.values[changed.roles.pool.values.indexOf(-1640531527)] += 1 + expect( + diagnoseNormalizeStringConcealing( + changed, + readWordcode(changed), + validateReferences(changed, readWordcode(changed)), + ), + ).toMatchObject({ + ok: false, + diagnostic: { code: 'string-concealing-shape' }, + }) + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(source)).toEqual(observe(encoded)) + expect(observe(source)).toEqual(observe(decoded.result.output)) + expect(record.effectObservation).toMatchObject({ + observed: true, + components: { stringConcealing: true, concealConstants: true }, + }) +}) + +test('restores strings and finalizer behavior through all four bytecode passes', () => { + const name = 'string-concealing-cff-dispatcher-concealed-constants' + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const record = JSON.parse(fixture(name, 'qualification.json')) + const container = diagnoseContainer(encoded).result + const wordcode = readWordcode(container) + const normalizedConstants = diagnoseNormalizeConcealedConstants( + encoded, + container, + wordcode, + ) + expect(normalizedConstants.ok).toBe(true) + const plain = normalizedConstants.result.container + const plainWordcode = readWordcode(plain) + expect( + partitionFunctions( + plain, + plainWordcode, + validateReferences(plain, plainWordcode), + ), + ).toBeNull() + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(source)).toEqual(observe(encoded)) + expect(observe(source)).toEqual(observe(decoded.result.output)) + expect(record.effectObservation).toMatchObject({ + observed: true, + components: { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + }, + }) +}) + +test('keeps a live branch into the middle of a string site as a decline', () => { + const encoded = fixture( + 'string-concealing-cff-dispatcher-exceptions', + 'encoded.js', + ) + let container = diagnoseContainer(encoded).result + for (const adapter of [ + diagnoseNormalizeDispatcherWordcode, + diagnoseNormalizeCffWordcode, + ]) { + const wordcode = readWordcode(container) + const references = validateReferences(container, wordcode) + const partition = diagnoseFunctionPartition( + container, + wordcode, + references, + { sharedParentSuspects: true }, + ) + expect(partition.ok).toBe(true) + const prepared = adapter(container, wordcode, partition.result) + expect(prepared.ok).toBe(true) + container = prepared.result.container + } + const wordcode = readWordcode(container) + const references = validateReferences(container, wordcode) + expect( + diagnoseNormalizeStringConcealing(container, wordcode, references).ok, + ).toBe(true) + const partition = diagnoseFunctionPartition(container, wordcode, references, { + sharedParentSuspects: true, + }).result + const body = wordcode.instructions.filter( + (item) => + item.pc >= partition.functions[1].startPc && + item.pc < partition.functions[1].endPc, + ) + // A site's three loads run straight into its call, directly or through a kept jump. + const callAt = (item) => + item?.name === 'JUMP' + ? body.find((target) => target.pc === item.words[1]) + : item + const loads = body.findIndex( + (item, index) => + body.slice(index, index + 3).every((load) => load.name === 'LOAD_INT') && + callAt(body[index + 3])?.name === 'CALL' && + callAt(body[index + 3]).words[3] === 4, + ) + expect(loads).toBeGreaterThan(-1) + const thirdLoad = body[loads + 2] + const branch = body.find((item) => item.name === 'JUMP_IF_FALSE') + expect(branch).toBeDefined() + const damaged = structuredClone(container) + damaged.roles.words.values[branch.pc + 2] = thirdLoad.pc + const changedWordcode = readWordcode(damaged) + const changedReferences = validateReferences(damaged, changedWordcode) + expect( + diagnoseNormalizeStringConcealing( + damaged, + changedWordcode, + changedReferences, + ), + ).toMatchObject({ + ok: false, + diagnostic: { code: 'string-concealing-shape' }, + }) +}) diff --git a/test/vm/jsconfuser-vm/normalize-string-concealing.test.js b/test/vm/jsconfuser-vm/normalize-string-concealing.test.js new file mode 100644 index 00000000..12c5b171 --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-string-concealing.test.js @@ -0,0 +1,151 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { validateReferences } from '../../../src/vm/jsconfuser-vm/validate-references.js' +import { partitionFunctions } from '../../../src/vm/jsconfuser-vm/partition-functions.js' +import { diagnoseNormalizeStringConcealing } from '../../../src/vm/jsconfuser-vm/normalize-string-concealing.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, Buffer, atob, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +function withWords(encoded, words) { + const original = encoded.match(/var BYTECODE = \[[\s\S]*?\];/)?.[0] + expect(original).toBeDefined() + return encoded.replace(original, `var BYTECODE = [${words.join(',')}];`) +} + +function analysesOf(encoded) { + const container = diagnoseContainer(encoded).result + const wordcode = readWordcode(container) + const references = validateReferences(container, wordcode) + return { container, wordcode, references } +} + +test.each([ + 'string-concealing-switch', + 'string-concealing-methods', + 'string-concealing-exceptions', + 'string-concealing-closures', + 'string-concealing-nested', +])('statically restores the shared bank in %s', (name) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const qualification = JSON.parse(fixture(name, 'qualification.json')) + const { container, wordcode, references } = analysesOf(encoded) + const original = JSON.stringify(container) + const normalized = diagnoseNormalizeStringConcealing( + container, + wordcode, + references, + ) + expect(normalized.ok).toBe(true) + expect(JSON.stringify(container)).toBe(original) + expect(normalized.result.decodedCount).toBeGreaterThan(0) + const next = normalized.result.container + const nextWordcode = readWordcode(next) + expect( + partitionFunctions( + next, + nextWordcode, + validateReferences(next, nextWordcode), + ), + ).not.toBeNull() + expect(nextWordcode.wordCount).toBeLessThan(wordcode.wordCount) + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, decoded.result.output).ok).toBe( + true, + ) + expect(observe(encoded)).toEqual(observe(source)) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(qualification.effectObservation).toEqual({ + axis: 'serialized-bytecode', + observed: true, + }) +}) + +test('declines a changed decode helper body atomically', () => { + const encoded = fixture('string-concealing-switch', 'encoded.js') + const { container, wordcode } = analysesOf(encoded) + const changed = container.roles.words.values.slice() + const helper = wordcode.instructions.find( + (item) => item.pc > 300 && item.name === 'USHR', + ) + changed[helper.pc] = container.roles.op.values.BXOR + const damaged = withWords(encoded, changed) + const next = analysesOf(damaged) + expect( + diagnoseNormalizeStringConcealing( + next.container, + next.wordcode, + next.references, + ).ok, + ).toBe(false) + expect(diagnoseStandalone(damaged)).toMatchObject({ ok: false, result: null }) +}) + +test('declines a noncanonical bank and a changed threaded capture', () => { + const encoded = fixture('string-concealing-switch', 'encoded.js') + const damagedBank = encoded.replace( + /(var CONSTANTS = \[\/\* 0 \*\/")([^"]+)/, + '$1!', + ) + expect(diagnoseStandalone(damagedBank)).toMatchObject({ + ok: false, + result: null, + }) + + const { container, wordcode } = analysesOf(encoded) + const capture = wordcode.instructions.find( + (item) => + item.name === 'MAKE_CLOSURE' && item.functionMeta.captureCount === 1, + ) + const changed = container.roles.words.values.slice() + changed[capture.pc + capture.width - 1] += 1 + const damagedCapture = withWords(encoded, changed) + expect(diagnoseStandalone(damagedCapture)).toMatchObject({ + ok: false, + result: null, + }) +}) + +test('declines a decode slice outside the padded bank', () => { + const encoded = fixture('string-concealing-switch', 'encoded.js') + const { container, wordcode } = analysesOf(encoded) + const site = wordcode.instructions.find( + (item, index) => + item.name === 'CALL' && + item.words[3] === 4 && + wordcode.instructions[index - 1]?.name === 'LOAD_INT', + ) + const prior = wordcode.instructions[wordcode.instructions.indexOf(site) - 2] + const changed = container.roles.words.values.slice() + changed[prior.pc + 2] = 0xffffffff + const damaged = withWords(encoded, changed) + expect(diagnoseStandalone(damaged)).toMatchObject({ ok: false, result: null }) +}) diff --git a/test/vm/jsconfuser-vm/numeric-to-vm-switch.test.js b/test/vm/jsconfuser-vm/numeric-to-vm-switch.test.js new file mode 100644 index 00000000..aae013f6 --- /dev/null +++ b/test/vm/jsconfuser-vm/numeric-to-vm-switch.test.js @@ -0,0 +1,27 @@ +import { test, expect } from 'vitest' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { diagnoseStandaloneInput } from '../../../src/vm/jsconfuser-vm/diagnose-standalone.js' +import { numericToVmSwitch } from '../../../src/vm/jsconfuser-vm/numeric-to-vm-switch.js' + +test('numeric frontend removes serialized opcode and operand words at the handoff', () => { + const path = fileURLToPath( + new URL('./fixtures/readability/arithmetic/encoded.js', import.meta.url), + ) + const preflight = diagnoseStandaloneInput(readFileSync(path, 'utf8')) + expect(preflight.ok).toBe(true) + const model = numericToVmSwitch(preflight.model) + for (const cases of model.instructionsByFunction.values()) { + for (const item of cases) { + expect(item).toHaveProperty('operation') + expect(item).not.toHaveProperty('name') + expect(item).not.toHaveProperty('opcode') + expect(item).not.toHaveProperty('operands') + expect(item).not.toHaveProperty('wordOperands') + expect(item).not.toHaveProperty('words') + } + } + // Control is the backend's to solve: the handoff carries cases only. + expect(model).not.toHaveProperty('controlByFunction') + expect(model).not.toHaveProperty('structuredControl') +}) diff --git a/test/vm/jsconfuser-vm/owned-chain-source.test.js b/test/vm/jsconfuser-vm/owned-chain-source.test.js new file mode 100644 index 00000000..feebf83f --- /dev/null +++ b/test/vm/jsconfuser-vm/owned-chain-source.test.js @@ -0,0 +1,203 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import generate from '@babel/generator' +import * as t from '@babel/types' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { diagnoseReferences } from '../../../src/vm/jsconfuser-vm/validate-references.js' +import { diagnoseFunctionPartition } from '../../../src/vm/jsconfuser-vm/partition-functions.js' +import { diagnoseStandaloneInput } from '../../../src/vm/jsconfuser-vm/diagnose-standalone.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' + +const fixture = (name) => + fs.readFileSync(`test/vm/jsconfuser-vm/fixtures/${name}/encoded.js`, 'utf8') +function changedWords(name, change) { + const source = fixture(name) + const container = diagnoseContainer(source).result + const wordcode = diagnoseWordcode(container).result + const words = [...wordcode.words] + change(words, wordcode.instructions, container) + const ast = parse(source) + let changed = false + traverse(ast, { + VariableDeclarator(path) { + // The words variable: the array literal holding the container's words. + const values = path.node.init?.elements?.map((node) => node?.value) + if ( + JSON.stringify(values) === JSON.stringify(container.roles.words.values) + ) { + expect(path.node.init.type).toBe('ArrayExpression') + path.node.init.elements = words.map((value) => t.numericLiteral(value)) + changed = true + } + }, + }) + expect(changed).toBe(true) + return generate(ast).code +} +function firstDecline(source) { + const container = diagnoseContainer(source) + if (!container.ok) return { component: 'extract-container', ...container } + const wordcode = diagnoseWordcode(container.result) + if (!wordcode.ok) return { component: 'read-wordcode', ...wordcode } + const references = diagnoseReferences(container.result, wordcode.result) + if (!references.ok) return { component: 'validate-references', ...references } + const functions = diagnoseFunctionPartition( + container.result, + wordcode.result, + references.result, + ) + return { component: 'partition-functions', ...functions } +} +const controls = [ + [ + 'unknown word', + 'reference', + (words) => { + words[0] = 61 + }, + ], + [ + 'truncated call', + 'corpus/raw/f-closures-state', + (words, instructions) => { + const call = instructions.find((x) => x.name === 'CALL') + words[call.pc + 3] = 0xffffffff + }, + ], + [ + 'constant outside pool', + 'reference', + (words, instructions, container) => { + const item = instructions.find((x) => x.name === 'LOAD_CONST') + words[item.pc + 2] = container.roles.pool.values.length + }, + ], + [ + 'entry inside operand', + 'corpus/raw/f-closures-state', + (words, instructions) => { + const item = instructions.find((x) => x.name === 'MAKE_CLOSURE') + words[item.pc + 2] = item.pc + 1 + }, + ], + [ + 'direct branch across functions', + 'corpus/raw/f-finally-abrupt', + (words, instructions) => { + const item = instructions.find((x) => x.name === 'JUMP') + words[item.pc + 1] = 0 + }, + ], +] +const expectedDiagnostics = { + 'unknown word': { + name: 'unknown word', + component: 'read-wordcode', + diagnostic: { + code: 'unknown-opcode', + message: 'Unknown opcode 61 at pc 0', + }, + publicDiagnostic: { + code: 'wordcode-declined', + message: 'readWordcode declined the input wordcode', + }, + }, + 'truncated call': { + name: 'truncated call', + component: 'read-wordcode', + diagnostic: { + code: 'truncated-instruction', + message: 'CALL at 12 is truncated in its argc payload', + }, + publicDiagnostic: { + code: 'wordcode-declined', + message: 'readWordcode declined the input wordcode', + }, + }, + 'constant outside pool': { + name: 'constant outside pool', + component: 'validate-references', + diagnostic: { + code: 'invalid-constant-reference', + message: 'LOAD_CONST@6 has an invalid constant pair', + }, + publicDiagnostic: { + code: 'references-declined', + message: 'validateReferences declined the input references', + }, + }, + 'entry inside operand': { + name: 'entry inside operand', + component: 'validate-references', + diagnostic: { + code: 'invalid-label-reference', + message: 'MAKE_CLOSURE@2 has an invalid entry PC', + }, + publicDiagnostic: { + code: 'references-declined', + message: 'validateReferences declined the input references', + }, + }, + 'direct branch across functions': { + name: 'direct branch across functions', + component: 'partition-functions', + diagnostic: { + code: 'cross-function-target', + message: 'JUMP@141 targets function 0 from 1', + }, + publicDiagnostic: { + code: 'functions-declined', + message: 'partitionFunctions declined the input function partition', + }, + }, +} +for (const [name, path, change] of controls) + test(name + ' declines through the normal source entry', () => { + const source = changedWords(path, change) + const first = firstDecline(source) + expect(first.ok, JSON.stringify(first.diagnostic)).toBe(false) + expect(first.result).toBeNull() + const publicResult = diagnoseStandaloneInput(source) + expect(publicResult.ok).toBe(false) + expect(first.component).toBe(expectedDiagnostics[name].component) + expect(first.diagnostic).toEqual(expectedDiagnostics[name].diagnostic) + expect(publicResult.diagnostic).toEqual( + expectedDiagnostics[name].publicDiagnostic, + ) + const decoded = diagnoseStandalone(source) + expect(decoded.ok).toBe(false) + expect(decoded.result).toBeNull() + expect(decoded.diagnostic).toEqual( + expectedDiagnostics[name].publicDiagnostic, + ) + }) +for (const name of [ + 'reference', + 'corpus/raw/f-closures-state', + 'corpus/raw/f-expression-mutation', + 'corpus/raw/f-for-in-enumeration', +]) + test( + name + ' preserves source behavior after the owned chain succeeds', + () => { + const source = fixture(name) + const diagnosis = diagnoseStandaloneInput(source) + expect(diagnosis.ok, diagnosis.diagnostic?.message).toBe(true) + const decoded = diagnoseStandalone(source) + expect(decoded.ok, decoded.diagnostic?.message).toBe(true) + const run = (code) => { + const window = {} + vm.runInNewContext( + code, + { window, Buffer, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return JSON.stringify(window.TEST_OUTPUT) + } + expect(run(decoded.result.output)).toBe(run(source)) + }, + ) diff --git a/test/vm/jsconfuser-vm/vm-copy-simplification.test.js b/test/vm/jsconfuser-vm/vm-copy-simplification.test.js new file mode 100644 index 00000000..762032b9 --- /dev/null +++ b/test/vm/jsconfuser-vm/vm-copy-simplification.test.js @@ -0,0 +1,144 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { diagnoseStandaloneInput } from '../../../src/vm/jsconfuser-vm/diagnose-standalone.js' +import { numericToVmSwitch } from '../../../src/vm/jsconfuser-vm/numeric-to-vm-switch.js' +import { solveControl } from '../../../src/vm/switch/vm-switch-control.js' + +function fixture(cellId, kind = 'raw') { + const directory = new URL( + `./fixtures/${kind === 'readability' ? 'readability' : `corpus/${kind}`}/${cellId}/`, + import.meta.url, + ) + return { + encoded: fs.readFileSync(new URL('encoded.js', directory), 'utf8'), + source: fs.readFileSync(new URL('source.js', directory), 'utf8'), + } +} + +function execute(source) { + const window = {} + window.window = window + const context = vm.createContext(window) + vm.runInContext(source, context, { timeout: 5000 }) + return window.TEST_OUTPUT +} + +function staticChecks(output) { + const ast = parseSuccessfulOutput(output) + expect(assertNoVmResidue(ast).ok).toBe(true) + expect(assertDerivedStateConsistency(ast, output).ok).toBe(true) +} + +// The frontend's instruction at a PC, by function. +function instructionAt(encoded) { + const { model } = diagnoseStandaloneInput(encoded) + return (functionId, pc) => + model.instructionsByFunction.get(functionId).find((item) => item.pc === pc) +} + +// The control solver's edges entering a case, by function. +function incomingCount(encoded) { + const model = numericToVmSwitch(diagnoseStandaloneInput(encoded).model) + const edges = new Map( + model.functions.functions.map((fn) => [ + fn.id, + solveControl(fn, model.instructionsByFunction.get(fn.id)).edges, + ]), + ) + return (functionId, pc) => + edges.get(functionId).filter(({ targetPc }) => targetPc === pc).length +} + +test('source-derived copy proofs simplify independent supported controls', () => { + for (const [cellId, kind] of [ + ['arithmetic', 'readability'], + ['f-literals-order', 'raw'], + ]) { + const input = fixture(cellId, kind) + const diagnosis = diagnoseStandalone(input.encoded) + expect(diagnosis.ok, cellId).toBe(true) + const { result } = diagnosis + expect(result.simplification.schemaVersion).toBe( + 'jsconfuser-vm-simplification.v1', + ) + expect(result.proof.freshSimplificationState).toBe(true) + expect(result.proof.simplificationDispositionsDisjoint).toBe(true) + expect(result.simplification.summary.safelyEliminated).toBeGreaterThan(0) + for (const proof of result.simplification.functions.flatMap( + ({ copies }) => copies, + )) { + expect(proof.disposition).toBe('safely-eliminated') + expect(proof.reachingDefinitions).toEqual([String(proof.producerPc)]) + expect(proof.useSites).toEqual([ + { pc: proof.movePc, register: proof.sourceRegister }, + ]) + expect(proof.producerRetained).toBe(true) + expect(proof.evaluationOrderPreserved).toBe(true) + expect(proof.receiverSemanticsPreserved).toBe(true) + } + staticChecks(result.output) + expect(execute(result.output)).toEqual(execute(input.source)) + } +}) + +test('joins and capture cells remain barriers to copy coalescing', () => { + const branching = fixture('f-branching') + const branchingDiagnosis = diagnoseStandalone(branching.encoded) + expect(branchingDiagnosis.ok).toBe(true) + const branchingResult = branchingDiagnosis.result + const branchingInstruction = instructionAt(branching.encoded) + const branchingIncoming = incomingCount(branching.encoded) + for (const proof of branchingResult.simplification.functions.flatMap( + ({ copies }) => copies, + )) { + expect(branchingInstruction(proof.functionId, proof.movePc)?.name).toBe( + 'MOVE', + ) + expect(branchingIncoming(proof.functionId, proof.movePc)).toBe(1) + } + expect(execute(branchingResult.output)).toEqual(execute(branching.source)) + + const closure = fixture('closure-loop', 'readability') + const closureDiagnosis = diagnoseStandalone(closure.encoded) + expect(closureDiagnosis.ok).toBe(true) + const closureFunctions = closureDiagnosis.result.simplification.functions + expect( + closureFunctions.find(({ functionId }) => functionId === 1).copies, + ).toEqual([]) + expect( + closureFunctions.find(({ functionId }) => functionId === 2).copies, + ).toEqual([]) + expect(closureDiagnosis.result.output).toContain( + 'const __recovered_cells_1 = [];', + ) + expect(execute(closureDiagnosis.result.output)).toEqual( + execute(closure.source), + ) +}) + +test('effectful producers are retained while only their private move is removed', () => { + const input = fixture('f-expression-mutation') + const diagnosis = diagnoseStandalone(input.encoded) + expect(diagnosis.ok).toBe(true) + const instruction = instructionAt(input.encoded) + const effectfulNames = new Set(['ADD', 'BUILD_ARRAY', 'BUILD_OBJECT']) + const effectfulProofs = diagnosis.result.simplification.functions + .flatMap(({ copies }) => copies) + .filter((proof) => { + const producer = instruction(proof.functionId, proof.producerPc) + return effectfulNames.has(producer?.name) + }) + expect(effectfulProofs.length).toBeGreaterThan(0) + expect( + effectfulProofs.every(({ producerRetained }) => producerRetained), + ).toBe(true) + staticChecks(diagnosis.result.output) + expect(execute(diagnosis.result.output)).toEqual(execute(input.source)) +}) diff --git a/test/vm/switch/fixtures/vm-switch-statements/exits/forms/encoded.js b/test/vm/switch/fixtures/vm-switch-statements/exits/forms/encoded.js new file mode 100644 index 00000000..8e893ea1 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/exits/forms/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */1, /* 1 */0, /* 2 */2, /* 3 */9, /* 4 */"window", /* 5 */"TEST_OUTPUT", /* 6 */"length", /* 7 */undefined, /* 8 */"value", /* 9 */"thrown", /* 10 */"message", /* 11 */"push", /* 12 */5, /* 13 */10, /* 14 */3, /* 15 */null, /* 16 */"Error", /* 17 */"s", /* 18 */"at", /* 19 */"none", /* 20 */"neg", /* 21 */"y"]; +var BYTECODE = [4, 1, 47, 16, 314, 1, 6, 0, 0, 5, 2, 16, 47, 16, 382, 1, 6, 0, 0, 5, 3, 16, 47, 16, 442, 1, 7, 0, 0, 5, 4, 16, 47, 16, 542, 1, 8, 0, 0, 5, 5, 16, 47, 16, 613, 1, 8, 0, 0, 5, 6, 16, 47, 16, 697, 1, 7, 0, 0, 5, 7, 16, 47, 16, 763, 1, 6, 0, 0, 5, 8, 16, 47, 16, 807, 1, 7, 0, 0, 5, 9, 16, 48, 16, 8, 2, 3, 4, 5, 6, 7, 8, 9, 5, 10, 16, 0, 16, 0, 0, 32, 17, 16, 0, 16, 1, 0, 0, 18, 2, 0, 0, 19, 3, 0, 48, 20, 4, 17, 16, 18, 19, 5, 11, 20, 2, 16, 4, 0, 0, 17, 5, 0, 48, 18, 0, 9, 16, 17, 18, 0, 16, 1, 0, 5, 12, 16, 0, 16, 6, 0, 8, 17, 10, 16, 22, 16, 12, 17, 40, 16, 308, 0, 16, 1, 0, 5, 13, 16, 0, 16, 6, 0, 8, 17, 11, 16, 22, 16, 13, 17, 40, 16, 292, 0, 14, 7, 0, 54, 220, 15, 0, 16, 8, 0, 8, 17, 10, 12, 8, 18, 11, 13, 43, 19, 10, 17, 1, 18, 48, 17, 2, 16, 19, 5, 14, 17, 55, 39, 240, 0, 16, 9, 0, 0, 17, 10, 0, 8, 18, 15, 17, 48, 17, 2, 16, 18, 5, 14, 17, 2, 16, 4, 0, 0, 17, 5, 0, 8, 18, 16, 17, 0, 16, 11, 0, 8, 17, 18, 16, 8, 16, 11, 13, 48, 19, 3, 12, 16, 14, 43, 16, 18, 17, 1, 19, 5, 16, 13, 0, 16, 0, 0, 11, 17, 13, 16, 5, 13, 17, 39, 169, 5, 16, 12, 0, 16, 0, 0, 11, 17, 12, 16, 5, 12, 17, 39, 147, 0, 16, 7, 0, 45, 16, 4, 2, 0, 4, 1, 0, 5, 3, 4, 0, 4, 12, 0, 22, 5, 3, 4, 40, 5, 367, 26, 4, 3, 0, 40, 4, 351, 0, 4, 13, 0, 13, 5, 3, 4, 45, 5, 5, 4, 3, 0, 4, 0, 0, 11, 5, 3, 4, 5, 3, 5, 39, 323, 0, 4, 0, 0, 32, 5, 4, 45, 5, 0, 4, 7, 0, 45, 4, 4, 2, 0, 4, 1, 0, 5, 3, 4, 0, 4, 12, 0, 22, 5, 3, 4, 40, 5, 427, 26, 4, 3, 0, 40, 4, 411, 45, 3, 5, 4, 3, 0, 4, 0, 0, 11, 5, 3, 4, 5, 3, 5, 39, 391, 0, 4, 0, 0, 32, 5, 4, 45, 5, 0, 4, 7, 0, 45, 4, 4, 2, 0, 5, 1, 0, 5, 3, 5, 0, 5, 14, 0, 22, 6, 3, 5, 40, 6, 530, 0, 5, 1, 0, 5, 4, 5, 0, 5, 14, 0, 22, 6, 4, 5, 40, 6, 514, 11, 5, 3, 4, 26, 6, 5, 0, 40, 6, 498, 48, 5, 2, 3, 4, 45, 5, 5, 5, 4, 0, 5, 0, 0, 11, 6, 4, 5, 5, 4, 6, 39, 469, 5, 5, 3, 0, 5, 0, 0, 11, 6, 3, 5, 5, 3, 6, 39, 451, 0, 5, 15, 0, 45, 5, 0, 5, 7, 0, 45, 5, 4, 2, 0, 5, 1, 0, 5, 3, 5, 0, 5, 1, 0, 5, 4, 5, 22, 5, 4, 0, 40, 5, 588, 11, 5, 3, 4, 5, 3, 5, 5, 5, 4, 0, 5, 0, 0, 11, 6, 4, 5, 5, 4, 6, 39, 558, 2, 5, 16, 0, 0, 6, 17, 0, 11, 7, 6, 3, 44, 6, 5, 1, 7, 46, 6, 0, 5, 7, 0, 45, 5, 4, 2, 0, 5, 1, 0, 5, 3, 5, 0, 5, 1, 0, 5, 4, 5, 0, 5, 12, 0, 22, 6, 4, 5, 40, 6, 689, 26, 5, 4, 0, 40, 5, 666, 2, 5, 16, 0, 0, 6, 18, 0, 11, 7, 6, 4, 44, 6, 5, 1, 7, 46, 6, 11, 5, 3, 4, 5, 3, 5, 5, 5, 4, 0, 5, 0, 0, 11, 6, 4, 5, 5, 4, 6, 39, 629, 45, 3, 0, 5, 7, 0, 45, 5, 4, 2, 0, 4, 1, 0, 5, 3, 4, 0, 4, 12, 0, 22, 5, 3, 4, 40, 5, 742, 26, 4, 3, 0, 40, 4, 726, 45, 3, 5, 4, 3, 0, 4, 0, 0, 11, 5, 3, 4, 5, 3, 5, 39, 706, 2, 4, 16, 0, 0, 5, 19, 0, 44, 6, 4, 1, 5, 46, 6, 0, 4, 7, 0, 45, 4, 4, 2, 0, 3, 1, 0, 22, 4, 0, 3, 40, 4, 791, 2, 3, 16, 0, 0, 4, 20, 0, 44, 5, 3, 1, 4, 46, 5, 0, 3, 0, 0, 11, 4, 0, 3, 45, 4, 0, 3, 7, 0, 45, 3, 4, 2, 0, 4, 1, 0, 5, 3, 4, 40, 0, 828, 0, 4, 0, 0, 5, 3, 4, 39, 835, 0, 4, 2, 0, 5, 3, 4, 2, 4, 16, 0, 0, 5, 21, 0, 11, 6, 5, 3, 44, 5, 4, 1, 6, 46, 5, 0, 4, 7, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 21; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-statements/exits/forms/source.js b/test/vm/switch/fixtures/vm-switch-statements/exits/forms/source.js new file mode 100644 index 00000000..1633006e --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/exits/forms/source.js @@ -0,0 +1,18 @@ +function returnInFor(n) { for (var i = 0; i < 5; i++) { if (i === n) return i * 10; } return -1; } +function returnInWhile(n) { var i = 0; while (i < 5) { if (i === n) return i; i++; } return -1; } +function returnInNested(n) { for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { if (i + j === n) return [i, j]; } } return null; } +function loopThenThrow(n) { var s = 0; for (var i = 0; i < n; i++) { s += i; } throw new Error("s" + s); } +function throwInLoopThenReturn(n) { var s = 0; for (var i = 0; i < 5; i++) { if (i === n) throw new Error("at" + i); s += i; } return s; } +function returnInLoopThenThrow(n) { for (var i = 0; i < 5; i++) { if (i === n) return i; } throw new Error("none"); } +function guard(v) { if (v < 0) throw new Error("neg"); return v + 1; } +function ifElseThenThrow(x) { var y = 0; if (x) { y = 1; } else { y = 2; } throw new Error("y" + y); } +var forms = [returnInFor, returnInWhile, returnInNested, loopThenThrow, throwInLoopThenReturn, returnInLoopThenThrow, guard, ifElseThenThrow]; +var inputs = [-1, 0, 2, 9]; +window.TEST_OUTPUT = []; +for (var index = 0; index < forms.length; index++) { + for (var k = 0; k < inputs.length; k++) { + var outcome; + try { outcome = ["value", forms[index](inputs[k])]; } catch (error) { outcome = ["thrown", error.message]; } + window.TEST_OUTPUT.push([index, inputs[k], outcome]); + } +} diff --git a/test/vm/switch/fixtures/vm-switch-statements/exits/generation.json b/test/vm/switch/fixtures/vm-switch-statements/exits/generation.json new file mode 100644 index 00000000..cf4ed5f7 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/exits/generation.json @@ -0,0 +1,11 @@ +{ + "note": "Each cell's encoded.js is the VM encoder's output for its source.js with default options, which draw no randomness, with the leading disassembly comment block removed (the committed corpus format). Regenerating with the same encoder revision reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5", + "options": {} + }, + "cells": { + "forms": "one function per way a statement can end inside a function: return inside for, while and nested loops; a loop then a final throw; a throw inside a loop then a final return; a return inside a loop then a final throw; a guard throw then a return; an if/else then a final throw; each run with inputs that take every path" + } +} diff --git a/test/vm/switch/fixtures/vm-switch-statements/loop-tests/forms/encoded.js b/test/vm/switch/fixtures/vm-switch-statements/loop-tests/forms/encoded.js new file mode 100644 index 00000000..72274ae0 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/loop-tests/forms/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"v", /* 1 */1, /* 2 */"next", /* 3 */2, /* 4 */3, /* 5 */null, /* 6 */"window", /* 7 */"TEST_OUTPUT", /* 8 */30, /* 9 */0, /* 10 */5, /* 11 */9, /* 12 */4, /* 13 */40, /* 14 */undefined, /* 15 */20, /* 16 */"length", /* 17 */"push", /* 18 */50]; +var BYTECODE = [4, 1, 47, 9, 362, 1, 8, 0, 0, 5, 2, 9, 47, 9, 438, 1, 8, 0, 0, 5, 3, 9, 47, 9, 518, 2, 10, 0, 0, 5, 4, 9, 47, 9, 619, 2, 10, 0, 0, 5, 5, 9, 47, 9, 717, 1, 8, 0, 0, 5, 6, 9, 47, 9, 782, 1, 10, 0, 0, 5, 7, 9, 0, 9, 0, 0, 0, 10, 1, 0, 0, 11, 2, 0, 0, 12, 0, 0, 0, 13, 3, 0, 0, 14, 2, 0, 0, 15, 0, 0, 0, 16, 4, 0, 0, 17, 2, 0, 0, 18, 5, 0, 49, 19, 2, 15, 16, 17, 18, 49, 15, 2, 12, 13, 14, 19, 49, 12, 2, 9, 10, 11, 15, 5, 8, 12, 2, 9, 6, 0, 0, 10, 7, 0, 0, 11, 4, 0, 42, 12, 2, 1, 11, 0, 11, 8, 0, 42, 13, 2, 1, 11, 0, 11, 9, 0, 42, 14, 3, 1, 11, 0, 11, 10, 0, 42, 15, 3, 1, 11, 0, 11, 1, 0, 0, 16, 10, 0, 0, 17, 3, 0, 0, 18, 11, 0, 48, 19, 4, 11, 16, 17, 18, 0, 11, 9, 0, 42, 16, 4, 2, 19, 11, 0, 11, 1, 0, 0, 17, 10, 0, 0, 18, 3, 0, 0, 19, 11, 0, 48, 20, 4, 11, 17, 18, 19, 0, 11, 12, 0, 42, 17, 4, 2, 20, 11, 0, 11, 9, 0, 42, 18, 5, 2, 8, 11, 0, 11, 4, 0, 42, 19, 5, 2, 8, 11, 0, 11, 9, 0, 42, 20, 6, 1, 11, 0, 11, 12, 0, 42, 21, 6, 1, 11, 0, 11, 1, 0, 0, 22, 3, 0, 0, 23, 9, 0, 0, 24, 11, 0, 48, 25, 4, 11, 22, 23, 24, 0, 11, 8, 0, 0, 22, 13, 0, 48, 23, 2, 11, 22, 0, 11, 10, 0, 48, 22, 1, 11, 48, 11, 3, 25, 23, 22, 42, 22, 7, 1, 11, 48, 11, 0, 42, 23, 7, 1, 11, 48, 11, 12, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 9, 9, 10, 11, 0, 9, 14, 0, 45, 9, 4, 2, 0, 5, 9, 0, 5, 3, 5, 0, 5, 9, 0, 5, 4, 5, 22, 5, 3, 0, 5, 6, 5, 40, 6, 399, 0, 5, 15, 0, 22, 7, 4, 5, 5, 6, 7, 40, 6, 425, 11, 5, 4, 3, 5, 4, 5, 5, 5, 3, 0, 5, 1, 0, 11, 6, 3, 5, 5, 3, 6, 39, 378, 48, 5, 2, 3, 4, 45, 5, 0, 5, 14, 0, 45, 5, 4, 2, 0, 5, 9, 0, 5, 3, 5, 0, 5, 9, 0, 5, 4, 5, 22, 5, 3, 0, 5, 6, 5, 41, 6, 475, 0, 5, 4, 0, 22, 7, 4, 5, 5, 6, 7, 40, 6, 505, 0, 5, 1, 0, 11, 6, 4, 5, 5, 4, 6, 5, 5, 3, 0, 5, 1, 0, 11, 6, 3, 5, 5, 3, 6, 39, 454, 48, 5, 2, 3, 4, 45, 5, 0, 5, 14, 0, 45, 5, 4, 3, 48, 6, 0, 5, 4, 6, 0, 6, 9, 0, 5, 5, 6, 0, 6, 16, 0, 8, 7, 0, 6, 22, 6, 5, 7, 5, 7, 6, 40, 7, 574, 5, 6, 1, 40, 6, 568, 8, 8, 0, 5, 23, 9, 8, 1, 5, 6, 9, 34, 8, 6, 5, 7, 8, 40, 7, 611, 0, 6, 17, 0, 8, 7, 4, 6, 8, 6, 0, 5, 43, 8, 4, 7, 1, 6, 5, 6, 5, 0, 6, 1, 0, 11, 7, 5, 6, 5, 5, 7, 39, 533, 45, 4, 0, 6, 14, 0, 45, 6, 4, 3, 48, 6, 0, 5, 4, 6, 5, 5, 0, 0, 6, 5, 0, 29, 7, 5, 6, 5, 6, 7, 40, 6, 671, 5, 7, 1, 40, 7, 665, 0, 8, 0, 0, 8, 9, 5, 8, 26, 8, 9, 1, 5, 7, 8, 34, 8, 7, 5, 6, 8, 40, 6, 709, 0, 6, 17, 0, 8, 7, 4, 6, 0, 6, 0, 0, 8, 8, 5, 6, 43, 6, 4, 7, 1, 8, 0, 6, 2, 0, 8, 7, 5, 6, 5, 5, 7, 39, 630, 45, 4, 0, 6, 14, 0, 45, 6, 4, 2, 0, 4, 9, 0, 5, 3, 4, 5, 4, 3, 0, 4, 1, 0, 11, 5, 3, 4, 5, 3, 5, 22, 4, 3, 0, 5, 5, 4, 41, 5, 769, 0, 4, 3, 0, 15, 6, 3, 4, 0, 4, 1, 0, 26, 7, 6, 4, 5, 5, 7, 40, 5, 774, 39, 726, 45, 3, 0, 4, 14, 0, 45, 4, 4, 2, 0, 6, 9, 0, 5, 3, 6, 0, 6, 9, 0, 5, 4, 6, 0, 6, 16, 0, 8, 7, 0, 6, 22, 6, 4, 7, 5, 7, 6, 40, 7, 827, 0, 6, 18, 0, 22, 8, 3, 6, 5, 7, 8, 40, 7, 928, 0, 6, 9, 0, 5, 5, 6, 8, 6, 0, 4, 0, 7, 16, 0, 8, 8, 6, 7, 22, 6, 5, 8, 5, 7, 6, 40, 7, 878, 8, 6, 0, 4, 8, 8, 6, 5, 0, 6, 9, 0, 27, 9, 8, 6, 5, 7, 9, 40, 7, 912, 8, 6, 0, 4, 8, 7, 6, 5, 11, 6, 3, 7, 5, 3, 6, 5, 6, 5, 0, 6, 1, 0, 11, 7, 5, 6, 5, 5, 7, 39, 837, 5, 6, 4, 0, 6, 1, 0, 11, 7, 4, 6, 5, 4, 7, 39, 798, 45, 3, 0, 6, 14, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 26; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-statements/loop-tests/forms/source.js b/test/vm/switch/fixtures/vm-switch-statements/loop-tests/forms/source.js new file mode 100644 index 00000000..66f74a04 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/loop-tests/forms/source.js @@ -0,0 +1,8 @@ +function whileAnd(n) { var i = 0, s = 0; while (i < n && s < 20) { s += i; i++; } return [i, s]; } +function whileOr(n) { var i = 0, s = 0; while (i < n || s < 3) { s += 1; i++; } return [i, s]; } +function forNested(list, limit) { var out = []; for (var k = 0; k < list.length && !(limit && list[k] > limit); k++) { out.push(list[k]); } return out; } +function walkChain(node, stop) { var out = []; for (var p = node; p != null && !(stop && p.v === stop); ) { out.push(p.v); p = p.next; } return out; } +function doWhileOr(n) { var i = 0; do { i++; } while (i < n || i % 2 === 1); return i; } +function nestedCompound(rows) { var total = 0; for (var r = 0; r < rows.length && total < 50; r++) { for (var c = 0; c < rows[r].length && rows[r][c] !== 0; c++) { total += rows[r][c]; } } return total; } +var chain = { v: 1, next: { v: 2, next: { v: 3, next: null } } }; +window.TEST_OUTPUT = [whileAnd(3), whileAnd(30), whileOr(0), whileOr(5), forNested([1, 5, 2, 9], 0), forNested([1, 5, 2, 9], 4), walkChain(chain, 0), walkChain(chain, 3), doWhileOr(0), doWhileOr(4), nestedCompound([[1, 2, 0, 9], [30, 40], [5]]), nestedCompound([])]; diff --git a/test/vm/switch/fixtures/vm-switch-statements/loop-tests/generation.json b/test/vm/switch/fixtures/vm-switch-statements/loop-tests/generation.json new file mode 100644 index 00000000..28dc7377 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/loop-tests/generation.json @@ -0,0 +1,11 @@ +{ + "note": "Each cell's encoded.js is the VM encoder's output for its source.js with default options, which draw no randomness, with the leading disassembly comment block removed (the committed corpus format). Regenerating with the same encoder revision reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5", + "options": {} + }, + "cells": { + "forms": "one function per loop whose test is a compound condition compiled as short-circuit branches: while with && and ||, for with && and a negated nested &&, a for with no update walking a chain, do-while with ||, and nested fors with compound tests; each run with inputs that take every path" + } +} diff --git a/test/vm/switch/fixtures/vm-switch-statements/nested-loops/forms/encoded.js b/test/vm/switch/fixtures/vm-switch-statements/nested-loops/forms/encoded.js new file mode 100644 index 00000000..3a2fef8d --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/nested-loops/forms/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"push", /* 1 */1, /* 2 */2, /* 3 */3, /* 4 */4, /* 5 */"window", /* 6 */"TEST_OUTPUT", /* 7 */undefined, /* 8 */0, /* 9 */"length"]; +var BYTECODE = [4, 1, 47, 10, 168, 0, 7, 0, 0, 5, 3, 10, 47, 10, 260, 0, 7, 0, 0, 5, 4, 10, 47, 10, 352, 0, 7, 0, 0, 5, 5, 10, 47, 10, 444, 0, 7, 0, 0, 5, 6, 10, 47, 10, 536, 0, 7, 0, 0, 5, 7, 10, 47, 10, 628, 0, 7, 0, 0, 5, 8, 10, 47, 10, 722, 1, 8, 0, 0, 5, 9, 10, 48, 10, 0, 5, 2, 10, 0, 10, 0, 0, 8, 11, 2, 10, 42, 10, 3, 0, 42, 12, 4, 0, 42, 13, 5, 0, 42, 14, 6, 0, 42, 15, 7, 0, 42, 16, 8, 0, 0, 17, 1, 0, 0, 18, 2, 0, 0, 19, 3, 0, 0, 20, 4, 0, 48, 21, 4, 17, 18, 19, 20, 42, 17, 9, 1, 21, 43, 18, 2, 11, 7, 10, 12, 13, 14, 15, 16, 17, 2, 10, 5, 0, 0, 11, 6, 0, 9, 10, 11, 2, 0, 10, 7, 0, 45, 10, 4, 1, 0, 5, 8, 0, 5, 2, 5, 0, 5, 8, 0, 5, 3, 5, 0, 5, 3, 0, 22, 6, 3, 5, 40, 6, 252, 0, 5, 8, 0, 5, 4, 5, 0, 5, 4, 0, 22, 6, 4, 5, 40, 6, 236, 11, 5, 2, 4, 5, 2, 5, 5, 5, 4, 0, 5, 1, 0, 11, 6, 4, 5, 5, 4, 6, 39, 202, 5, 5, 3, 0, 5, 1, 0, 11, 6, 3, 5, 5, 3, 6, 39, 184, 45, 2, 0, 5, 7, 0, 45, 5, 4, 1, 0, 5, 8, 0, 5, 2, 5, 0, 5, 8, 0, 5, 3, 5, 0, 5, 3, 0, 22, 6, 3, 5, 40, 6, 344, 0, 5, 8, 0, 5, 4, 5, 0, 5, 4, 0, 22, 6, 4, 5, 40, 6, 328, 11, 5, 2, 4, 5, 2, 5, 5, 5, 4, 0, 5, 1, 0, 11, 6, 4, 5, 5, 4, 6, 39, 294, 5, 5, 3, 0, 5, 1, 0, 11, 6, 3, 5, 5, 3, 6, 39, 276, 45, 2, 0, 5, 7, 0, 45, 5, 4, 1, 0, 5, 8, 0, 5, 2, 5, 0, 5, 8, 0, 5, 3, 5, 0, 5, 8, 0, 5, 4, 5, 11, 5, 2, 4, 5, 2, 5, 5, 5, 4, 0, 5, 1, 0, 11, 6, 4, 5, 5, 4, 6, 0, 5, 4, 0, 22, 6, 4, 5, 40, 6, 409, 39, 375, 5, 5, 3, 0, 5, 1, 0, 11, 6, 3, 5, 5, 3, 6, 0, 5, 3, 0, 22, 6, 3, 5, 40, 6, 436, 39, 368, 45, 2, 0, 5, 7, 0, 45, 5, 4, 1, 0, 5, 8, 0, 5, 2, 5, 0, 5, 8, 0, 5, 3, 5, 0, 5, 3, 0, 22, 6, 3, 5, 40, 6, 528, 0, 5, 8, 0, 5, 4, 5, 0, 5, 4, 0, 22, 6, 4, 5, 40, 6, 512, 11, 5, 2, 4, 5, 2, 5, 5, 5, 4, 0, 5, 1, 0, 11, 6, 4, 5, 5, 4, 6, 39, 478, 5, 5, 3, 0, 5, 1, 0, 11, 6, 3, 5, 5, 3, 6, 39, 460, 45, 2, 0, 5, 7, 0, 45, 5, 4, 1, 0, 5, 8, 0, 5, 2, 5, 0, 5, 8, 0, 5, 3, 5, 0, 5, 3, 0, 22, 6, 3, 5, 40, 6, 620, 0, 5, 8, 0, 5, 4, 5, 0, 5, 4, 0, 22, 6, 4, 5, 40, 6, 604, 11, 5, 2, 4, 5, 2, 5, 5, 5, 4, 0, 5, 1, 0, 11, 6, 4, 5, 5, 4, 6, 39, 570, 5, 5, 3, 0, 5, 1, 0, 11, 6, 3, 5, 5, 3, 6, 39, 552, 45, 2, 0, 5, 7, 0, 45, 5, 4, 1, 0, 5, 8, 0, 5, 2, 5, 0, 5, 8, 0, 5, 3, 5, 0, 5, 3, 0, 22, 6, 3, 5, 40, 6, 714, 0, 5, 8, 0, 5, 4, 5, 0, 5, 4, 0, 25, 6, 4, 5, 40, 6, 675, 39, 698, 11, 5, 2, 4, 5, 2, 5, 5, 5, 4, 0, 5, 1, 0, 11, 6, 4, 5, 5, 4, 6, 39, 662, 5, 5, 3, 0, 5, 1, 0, 11, 6, 3, 5, 5, 3, 6, 39, 644, 45, 2, 0, 5, 7, 0, 45, 5, 4, 2, 0, 6, 8, 0, 5, 3, 6, 0, 4, 7, 0, 0, 5, 7, 0, 0, 6, 8, 0, 5, 4, 6, 0, 6, 9, 0, 8, 7, 0, 6, 22, 6, 4, 7, 40, 6, 843, 0, 6, 8, 0, 5, 5, 6, 0, 6, 4, 0, 22, 7, 5, 6, 40, 7, 830, 0, 6, 2, 0, 22, 7, 5, 6, 40, 7, 803, 8, 6, 0, 4, 11, 7, 3, 6, 5, 3, 7, 39, 814, 0, 6, 1, 0, 12, 7, 3, 6, 5, 3, 7, 5, 6, 5, 0, 6, 1, 0, 11, 7, 5, 6, 5, 5, 7, 39, 768, 0, 6, 2, 0, 11, 7, 4, 6, 5, 4, 7, 39, 746, 45, 3, 0, 6, 7, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 22; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-statements/nested-loops/forms/source.js b/test/vm/switch/fixtures/vm-switch-statements/nested-loops/forms/source.js new file mode 100644 index 00000000..9a904987 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/nested-loops/forms/source.js @@ -0,0 +1,10 @@ +var out = []; +function forInFor() { var s = 0; for (var i = 0; i < 3; i++) { for (var j = 0; j < 4; j++) { s += j; } } return s; } +function whileInWhile() { var s = 0, i = 0; while (i < 3) { var j = 0; while (j < 4) { s += j; j++; } i++; } return s; } +function doWhileInDoWhile() { var s = 0, i = 0; do { var j = 0; do { s += j; j++; } while (j < 4); i++; } while (i < 3); return s; } +function whileInFor() { var s = 0; for (var i = 0; i < 3; i++) { var j = 0; while (j < 4) { s += j; j++; } } return s; } +function forInWhile() { var s = 0, i = 0; while (i < 3) { for (var j = 0; j < 4; j++) { s += j; } i++; } return s; } +function breakInInner() { var s = 0; for (var i = 0; i < 3; i++) { for (var j = 0; ; j++) { if (j >= 4) break; s += j; } } return s; } +function guardInInner(m) { var s = 0, i, j; for (i = 0; i < m.length; i += 2) { for (j = 0; j < 4; j++) { if (j < 2) s += m[i]; else s -= 1; } } return s; } +out.push(forInFor(), whileInWhile(), doWhileInDoWhile(), whileInFor(), forInWhile(), breakInInner(), guardInInner([1, 2, 3, 4])); +window.TEST_OUTPUT = out; diff --git a/test/vm/switch/fixtures/vm-switch-statements/nested-loops/generation.json b/test/vm/switch/fixtures/vm-switch-statements/nested-loops/generation.json new file mode 100644 index 00000000..b4e29634 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/nested-loops/generation.json @@ -0,0 +1,11 @@ +{ + "note": "Each cell's encoded.js is the VM encoder's output for its source.js with default options, which draw no randomness, with the leading disassembly comment block removed (the committed corpus format). Regenerating with the same encoder revision reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5", + "options": {} + }, + "cells": { + "forms": "one function per loop nesting (for in for, while in while, do-while in do-while, while in for, for in while, break in an inner loop, a conditional inside an inner loop)" + } +} diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/dowhile-continue/encoded.js b/test/vm/switch/fixtures/vm-switch-statements/try-ends/dowhile-continue/encoded.js new file mode 100644 index 00000000..383ae8e4 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/dowhile-continue/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */false, /* 3 */true, /* 4 */undefined, /* 5 */"push", /* 6 */0, /* 7 */1, /* 8 */"i", /* 9 */"f", /* 10 */"after", /* 11 */3, /* 12 */"done"]; +var BYTECODE = [4, 1, 47, 5, 74, 1, 6, 1, 0, 1, 2, 5, 3, 5, 47, 5, 99, 1, 9, 1, 0, 1, 3, 5, 4, 5, 48, 5, 0, 5, 2, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 48, 7, 3, 8, 9, 2, 9, 5, 6, 7, 0, 5, 4, 0, 45, 5, 4, 2, 3, 3, 0, 0, 4, 5, 0, 8, 5, 3, 4, 43, 4, 3, 5, 1, 0, 0, 3, 4, 0, 45, 3, 4, 2, 0, 6, 6, 0, 5, 3, 6, 59, 152, 4, 5, 170, 0, 6, 7, 0, 26, 7, 3, 6, 40, 7, 130, 1, 4, 172, 55, 39, 152, 3, 6, 0, 0, 7, 8, 0, 11, 8, 7, 3, 42, 7, 6, 1, 8, 55, 1, 4, 174, 39, 152, 3, 6, 0, 0, 7, 9, 0, 11, 8, 7, 3, 42, 7, 6, 1, 8, 58, 4, 46, 5, 39, 190, 3, 6, 0, 0, 7, 10, 0, 11, 8, 7, 3, 42, 7, 6, 1, 8, 0, 6, 7, 0, 11, 7, 3, 6, 5, 3, 7, 0, 6, 11, 0, 22, 8, 7, 6, 40, 8, 214, 39, 108, 3, 6, 0, 0, 7, 12, 0, 42, 8, 6, 1, 7, 45, 3, 0, 6, 4, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/dowhile-continue/source.js b/test/vm/switch/fixtures/vm-switch-statements/try-ends/dowhile-continue/source.js new file mode 100644 index 00000000..a1052574 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/dowhile-continue/source.js @@ -0,0 +1,3 @@ +var log = []; function note(v) { log.push(v); } +function f(x) { var i = 0; do { try { if (i === 1) continue; note("i" + i); } finally { note("f" + i); } note("after" + i); } while (++i < 3); note("done"); return i; } +window.TEST_OUTPUT = [f(false), f(true), log]; diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms-cff/encoded.js b/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms-cff/encoded.js new file mode 100644 index 00000000..b70e40a7 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms-cff/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */58675, /* 1 */0, /* 2 */37588, /* 3 */58367, /* 4 */28194, /* 5 */25004, /* 6 */49575, /* 7 */6328, /* 8 */45741, /* 9 */6212, /* 10 */22574, /* 11 */19415, /* 12 */29152, /* 13 */15561, /* 14 */12710, /* 15 */13103, /* 16 */31052, /* 17 */55166, /* 18 */59949, /* 19 */20298, /* 20 */41442, /* 21 */62432, /* 22 */58130, /* 23 */57303, /* 24 */7535, /* 25 */35302, /* 26 */17652, /* 27 */59356, /* 28 */55522, /* 29 */8076, /* 30 */894, /* 31 */290, /* 32 */5593, /* 33 */3477, /* 34 */19195, /* 35 */2, /* 36 */1, /* 37 */"window", /* 38 */"TEST_OUTPUT", /* 39 */"value", /* 40 */undefined, /* 41 */"length", /* 42 */"thrown", /* 43 */"message", /* 44 */"push", /* 45 */48570, /* 46 */32784, /* 47 */12310, /* 48 */3048, /* 49 */33212, /* 50 */13124, /* 51 */22224, /* 52 */2422, /* 53 */36676, /* 54 */17777, /* 55 */9480, /* 56 */5591, /* 57 */19466, /* 58 */22810, /* 59 */"try", /* 60 */"Error", /* 61 */"x", /* 62 */"catch", /* 63 */"after", /* 64 */2728, /* 65 */45774, /* 66 */59784, /* 67 */16366, /* 68 */32542, /* 69 */8148, /* 70 */4270, /* 71 */29478, /* 72 */12645, /* 73 */1347, /* 74 */5056, /* 75 */11946, /* 76 */2600, /* 77 */"always", /* 78 */"caught ", /* 79 */34802, /* 80 */41200, /* 81 */8119, /* 82 */1103, /* 83 */40019, /* 84 */17033, /* 85 */3645, /* 86 */29387, /* 87 */44588, /* 88 */31723, /* 89 */14199, /* 90 */22218, /* 91 */20946, /* 92 */3537, /* 93 */24965, /* 94 */40823, /* 95 */"finally", /* 96 */63210, /* 97 */591, /* 98 */16295, /* 99 */20813, /* 100 */3339, /* 101 */471, /* 102 */30928, /* 103 */16754, /* 104 */18462, /* 105 */8532, /* 106 */45829, /* 107 */22466, /* 108 */19619, /* 109 */58807, /* 110 */63163, /* 111 */"returned", /* 112 */27112, /* 113 */58571, /* 114 */27276, /* 115 */21928, /* 116 */59939, /* 117 */33585, /* 118 */7301, /* 119 */2900, /* 120 */10315, /* 121 */7598, /* 122 */9505, /* 123 */4967, /* 124 */36052, /* 125 */47181, /* 126 */3634, /* 127 */29861, /* 128 */13796, /* 129 */1737, /* 130 */4, /* 131 */"next", /* 132 */"i", /* 133 */63236, /* 134 */18553, /* 135 */5576, /* 136 */1359, /* 137 */14322, /* 138 */27195, /* 139 */40128, /* 140 */54778, /* 141 */18714, /* 142 */37121, /* 143 */57839, /* 144 */52253, /* 145 */2364, /* 146 */7519, /* 147 */3729, /* 148 */59267, /* 149 */9649, /* 150 */24560, /* 151 */6039, /* 152 */27883, /* 153 */35145, /* 154 */49318, /* 155 */874, /* 156 */3, /* 157 */"done", /* 158 */"f", /* 159 */47825, /* 160 */48295, /* 161 */22105, /* 162 */8, /* 163 */30769, /* 164 */9641, /* 165 */41882, /* 166 */13510, /* 167 */10129, /* 168 */17917, /* 169 */14901, /* 170 */12804, /* 171 */7213, /* 172 */970, /* 173 */33560, /* 174 */11846, /* 175 */14782, /* 176 */2312, /* 177 */43655, /* 178 */29499, /* 179 */4486, /* 180 */6]; +var BYTECODE = [4, 32, 0, 33, 0, 0, 5, 34, 33, 0, 35, 1, 0, 5, 36, 35, 0, 37, 2, 0, 27, 38, 34, 37, 40, 38, 665, 0, 39, 3, 0, 5, 36, 39, 26, 40, 34, 36, 40, 40, 43, 39, 813, 0, 41, 4, 0, 12, 42, 36, 41, 5, 36, 42, 26, 43, 34, 36, 40, 43, 63, 39, 1193, 0, 44, 5, 0, 12, 45, 36, 44, 5, 36, 45, 26, 46, 34, 36, 40, 46, 83, 39, 1107, 0, 47, 6, 0, 11, 48, 36, 47, 5, 36, 48, 26, 49, 34, 36, 40, 49, 103, 39, 1156, 0, 50, 7, 0, 11, 51, 36, 50, 5, 36, 51, 26, 52, 34, 36, 40, 52, 123, 39, 1136, 0, 53, 8, 0, 12, 54, 36, 53, 5, 36, 54, 26, 55, 34, 36, 40, 55, 143, 39, 997, 0, 56, 9, 0, 12, 57, 36, 56, 5, 36, 57, 26, 58, 34, 36, 40, 58, 163, 39, 710, 0, 59, 10, 0, 11, 60, 36, 59, 5, 36, 60, 26, 61, 34, 36, 40, 61, 183, 39, 837, 0, 62, 11, 0, 11, 63, 36, 62, 5, 36, 63, 26, 64, 34, 36, 40, 64, 203, 39, 1230, 0, 65, 12, 0, 12, 66, 36, 65, 5, 36, 66, 26, 67, 34, 36, 40, 67, 223, 39, 772, 0, 68, 13, 0, 11, 69, 36, 68, 5, 36, 69, 26, 70, 34, 36, 40, 70, 243, 39, 1021, 0, 71, 14, 0, 12, 72, 36, 71, 5, 36, 72, 26, 73, 34, 36, 40, 73, 263, 39, 974, 0, 74, 15, 0, 11, 75, 36, 74, 5, 36, 75, 26, 76, 34, 36, 40, 76, 283, 39, 719, 0, 77, 16, 0, 12, 78, 36, 77, 5, 36, 78, 26, 79, 34, 36, 40, 79, 303, 39, 1254, 0, 80, 17, 0, 11, 81, 36, 80, 5, 36, 81, 26, 82, 34, 36, 40, 82, 323, 39, 886, 0, 83, 18, 0, 12, 84, 36, 83, 5, 36, 84, 26, 85, 34, 36, 40, 85, 343, 39, 1221, 0, 86, 19, 0, 11, 87, 36, 86, 5, 36, 87, 26, 88, 34, 36, 40, 88, 363, 39, 846, 0, 89, 20, 0, 11, 90, 36, 89, 5, 36, 90, 26, 91, 34, 36, 40, 91, 383, 39, 914, 0, 92, 21, 0, 12, 93, 36, 92, 5, 36, 93, 26, 94, 34, 36, 40, 94, 403, 39, 1060, 0, 95, 22, 0, 11, 96, 36, 95, 5, 36, 96, 26, 97, 34, 36, 40, 97, 423, 39, 1027, 0, 98, 23, 0, 12, 99, 36, 98, 5, 36, 99, 26, 100, 34, 36, 40, 100, 443, 39, 1278, 0, 101, 24, 0, 11, 102, 36, 101, 5, 36, 102, 26, 103, 34, 36, 40, 103, 463, 39, 898, 0, 104, 25, 0, 11, 105, 36, 104, 5, 36, 105, 26, 106, 34, 36, 40, 106, 483, 39, 1077, 0, 107, 26, 0, 11, 108, 36, 107, 5, 36, 108, 26, 109, 34, 36, 40, 109, 503, 39, 743, 0, 110, 27, 0, 12, 111, 36, 110, 5, 36, 111, 26, 112, 34, 36, 40, 112, 523, 39, 680, 0, 113, 28, 0, 11, 114, 36, 113, 5, 36, 114, 26, 115, 34, 36, 40, 115, 543, 39, 792, 0, 116, 29, 0, 12, 117, 36, 116, 5, 36, 117, 26, 118, 34, 36, 40, 118, 563, 39, 665, 0, 119, 30, 0, 12, 120, 36, 119, 5, 36, 120, 26, 121, 34, 36, 40, 121, 583, 39, 866, 0, 122, 31, 0, 12, 123, 36, 122, 5, 36, 123, 26, 124, 34, 36, 40, 124, 603, 39, 1181, 0, 125, 32, 0, 11, 126, 36, 125, 5, 36, 126, 26, 127, 34, 36, 40, 127, 623, 39, 1127, 0, 128, 33, 0, 11, 129, 36, 128, 5, 36, 129, 26, 130, 34, 36, 40, 130, 643, 39, 951, 0, 131, 34, 0, 12, 132, 36, 131, 5, 36, 132, 26, 133, 34, 36, 40, 133, 663, 39, 933, 39, 16, 43, 135, 27, 28, 1, 29, 1, 134, 45620, 12, 34, 34, 134, 39, 16, 47, 135, 2805, 1, 55, 1, 0, 1, 3, 5, 7, 135, 47, 17, 3314, 1, 66, 1, 0, 1, 3, 1, 134, 11988, 11, 34, 34, 134, 39, 16, 1, 134, 12837, 11, 34, 34, 134, 39, 16, 47, 135, 4773, 1, 80, 1, 0, 1, 3, 5, 10, 135, 48, 18, 0, 1, 134, 16834, 11, 34, 34, 134, 39, 16, 0, 135, 35, 0, 22, 136, 13, 135, 41, 136, 763, 1, 134, 53580, 12, 34, 34, 134, 39, 16, 1, 134, 1116, 11, 34, 34, 134, 39, 16, 5, 135, 12, 0, 135, 36, 0, 11, 31, 12, 135, 1, 134, 40068, 11, 34, 34, 134, 39, 16, 2, 135, 37, 0, 0, 136, 38, 0, 8, 27, 135, 136, 1, 134, 52007, 12, 34, 34, 134, 39, 16, 5, 5, 16, 47, 135, 2232, 1, 57, 1, 0, 1, 3, 5, 6, 135, 1, 134, 55024, 12, 34, 34, 134, 39, 16, 1, 134, 9737, 12, 34, 34, 134, 39, 16, 9, 19, 20, 21, 0, 135, 1, 0, 5, 12, 135, 1, 134, 37140, 11, 34, 34, 134, 39, 16, 54, 1230, 15, 0, 22, 39, 0, 8, 23, 11, 12, 1, 134, 25088, 12, 34, 34, 134, 39, 16, 5, 12, 31, 1, 134, 2511, 12, 34, 34, 134, 39, 16, 0, 135, 1, 0, 5, 13, 135, 1, 134, 52954, 11, 34, 34, 134, 39, 16, 48, 135, 0, 5, 2, 135, 0, 14, 40, 0, 1, 134, 13920, 12, 34, 34, 134, 39, 16, 48, 135, 2, 22, 24, 5, 14, 135, 55, 1, 134, 19385, 11, 34, 34, 134, 39, 16, 4, 1, 47, 135, 1287, 1, 6, 1, 0, 1, 2, 5, 3, 135, 1, 134, 13628, 12, 34, 34, 134, 39, 16, 0, 135, 36, 0, 26, 136, 13, 135, 43, 24, 11, 23, 1, 136, 1, 134, 14673, 11, 34, 34, 134, 39, 16, 5, 8, 17, 47, 135, 3961, 1, 86, 1, 0, 1, 3, 5, 9, 135, 1, 134, 22579, 11, 34, 34, 134, 39, 16, 0, 135, 40, 0, 45, 135, 0, 135, 41, 0, 8, 136, 11, 135, 22, 135, 12, 136, 41, 135, 1051, 1, 134, 21996, 12, 34, 34, 134, 39, 16, 1, 134, 49768, 12, 34, 34, 134, 39, 16, 48, 135, 2, 25, 26, 5, 14, 135, 1, 134, 57482, 11, 34, 34, 134, 39, 16, 47, 135, 1312, 1, 50, 1, 0, 1, 3, 5, 4, 135, 47, 16, 1783, 1, 51, 1, 0, 1, 3, 1, 134, 13320, 11, 34, 34, 134, 39, 16, 5, 135, 13, 0, 135, 36, 0, 11, 30, 13, 135, 1, 134, 44436, 11, 34, 34, 134, 39, 16, 1, 134, 17681, 12, 34, 34, 134, 39, 16, 2, 19, 37, 0, 0, 20, 38, 0, 48, 21, 0, 1, 134, 38699, 12, 34, 34, 134, 39, 16, 5, 2, 18, 48, 135, 7, 4, 5, 6, 7, 8, 9, 10, 5, 11, 135, 1, 134, 6328, 11, 34, 34, 134, 39, 16, 5, 13, 30, 1, 134, 13094, 11, 34, 34, 134, 39, 16, 1, 134, 50511, 22, 134, 34, 134, 41, 134, 1212, 1, 134, 28790, 12, 34, 34, 134, 39, 16, 1, 134, 20428, 12, 34, 34, 134, 39, 16, 1, 134, 48714, 11, 34, 34, 134, 39, 16, 1, 34, 51108, 0, 25, 42, 0, 0, 135, 43, 0, 8, 26, 15, 135, 1, 134, 49725, 12, 34, 34, 134, 39, 16, 0, 135, 44, 0, 8, 28, 27, 135, 48, 29, 4, 12, 13, 14, 2, 1, 134, 43931, 11, 34, 34, 134, 39, 16, 1, 134, 29483, 11, 34, 34, 134, 39, 16, 4, 2, 3, 3, 0, 0, 4, 44, 0, 8, 5, 3, 4, 43, 4, 3, 5, 1, 0, 0, 3, 40, 0, 45, 3, 4, 4, 0, 5, 45, 0, 5, 6, 5, 0, 7, 1, 0, 5, 8, 7, 0, 9, 46, 0, 27, 10, 6, 9, 40, 10, 1577, 0, 11, 47, 0, 5, 8, 11, 26, 12, 6, 8, 40, 12, 1355, 39, 1622, 0, 13, 48, 0, 11, 14, 8, 13, 5, 8, 14, 26, 15, 6, 8, 40, 15, 1375, 39, 1697, 0, 16, 49, 0, 11, 17, 8, 16, 5, 8, 17, 26, 18, 6, 8, 40, 18, 1395, 39, 1730, 0, 19, 50, 0, 12, 20, 8, 19, 5, 8, 20, 26, 21, 6, 8, 40, 21, 1415, 39, 1577, 0, 22, 51, 0, 11, 23, 8, 22, 5, 8, 23, 26, 24, 6, 8, 40, 24, 1435, 39, 1660, 0, 25, 52, 0, 11, 26, 8, 25, 5, 8, 26, 26, 27, 6, 8, 40, 27, 1455, 39, 1777, 0, 28, 53, 0, 12, 29, 8, 28, 5, 8, 29, 26, 30, 6, 8, 40, 30, 1475, 39, 1613, 0, 31, 54, 0, 12, 32, 8, 31, 5, 8, 32, 26, 33, 6, 8, 40, 33, 1495, 39, 1650, 0, 34, 55, 0, 11, 35, 8, 34, 5, 8, 35, 26, 36, 6, 8, 40, 36, 1515, 39, 1688, 0, 37, 56, 0, 11, 38, 8, 37, 5, 8, 38, 26, 39, 6, 8, 40, 39, 1535, 39, 1598, 0, 40, 57, 0, 11, 41, 8, 40, 5, 8, 41, 26, 42, 6, 8, 40, 42, 1555, 39, 1706, 0, 43, 58, 0, 11, 44, 8, 43, 5, 8, 44, 26, 45, 6, 8, 40, 45, 1575, 39, 1756, 39, 1328, 3, 47, 0, 0, 48, 59, 0, 42, 49, 47, 1, 48, 1, 46, 29807, 12, 6, 6, 46, 39, 1328, 2, 47, 60, 0, 0, 48, 61, 0, 44, 49, 47, 1, 48, 46, 49, 1, 46, 39570, 11, 6, 6, 46, 39, 1328, 1, 46, 9793, 22, 46, 6, 46, 41, 46, 1641, 1, 46, 50676, 11, 6, 6, 46, 39, 1328, 1, 46, 11106, 11, 6, 6, 46, 39, 1328, 55, 1, 46, 57347, 11, 6, 6, 46, 39, 1328, 1, 46, 112, 22, 46, 6, 46, 41, 46, 1679, 1, 46, 45360, 12, 6, 6, 46, 39, 1328, 1, 46, 0, 11, 6, 6, 46, 39, 1328, 1, 46, 2809, 12, 6, 6, 46, 39, 1328, 1, 46, 44734, 11, 6, 6, 46, 39, 1328, 1, 6, 40176, 3, 47, 0, 0, 48, 62, 0, 42, 49, 47, 1, 48, 1, 46, 22810, 11, 6, 6, 46, 39, 1328, 4, 2, 54, 1706, 3, 41, 0, 1747, 1, 46, 13124, 12, 6, 6, 46, 39, 1328, 1, 46, 27860, 12, 6, 6, 46, 39, 1328, 3, 47, 0, 0, 48, 63, 0, 42, 49, 47, 1, 48, 1, 46, 2894, 12, 6, 6, 46, 39, 1328, 0, 47, 40, 0, 45, 47, 4, 8, 0, 9, 64, 0, 5, 10, 9, 0, 11, 1, 0, 5, 12, 11, 0, 13, 65, 0, 27, 14, 10, 13, 40, 14, 2028, 0, 15, 66, 0, 5, 12, 15, 26, 16, 10, 12, 40, 16, 1826, 39, 2132, 0, 17, 67, 0, 12, 18, 12, 17, 5, 12, 18, 26, 19, 10, 12, 40, 19, 1846, 39, 2028, 0, 20, 68, 0, 12, 21, 12, 20, 5, 12, 21, 26, 22, 10, 12, 40, 22, 1866, 39, 2067, 0, 23, 69, 0, 12, 24, 12, 23, 5, 12, 24, 26, 25, 10, 12, 40, 25, 1886, 39, 2163, 0, 26, 70, 0, 11, 27, 12, 26, 5, 12, 27, 26, 28, 10, 12, 40, 28, 1906, 39, 2076, 0, 29, 71, 0, 11, 30, 12, 29, 5, 12, 30, 26, 31, 10, 12, 40, 31, 1926, 39, 2142, 0, 32, 72, 0, 12, 33, 12, 32, 5, 12, 33, 26, 34, 10, 12, 40, 34, 1946, 39, 2126, 0, 35, 73, 0, 12, 36, 12, 35, 5, 12, 36, 26, 37, 10, 12, 40, 37, 1966, 39, 2056, 0, 38, 74, 0, 12, 39, 12, 38, 5, 12, 39, 26, 40, 10, 12, 40, 40, 1986, 39, 2104, 0, 41, 75, 0, 12, 42, 12, 41, 5, 12, 42, 26, 43, 10, 12, 40, 43, 2006, 39, 2209, 0, 44, 76, 0, 12, 45, 12, 44, 5, 12, 45, 26, 46, 10, 12, 40, 46, 2026, 39, 2181, 39, 1799, 1, 47, 6835, 22, 47, 10, 47, 41, 47, 2047, 1, 47, 40536, 12, 10, 10, 47, 39, 1799, 1, 47, 36420, 12, 10, 10, 47, 39, 1799, 0, 48, 77, 0, 44, 49, 4, 1, 48, 46, 49, 1, 47, 12955, 11, 10, 10, 47, 39, 1799, 1, 47, 4002, 22, 47, 10, 47, 41, 47, 2095, 1, 47, 0, 11, 10, 10, 47, 39, 1799, 1, 47, 52786, 11, 10, 10, 47, 39, 1799, 8, 48, 3, 7, 11, 49, 6, 48, 42, 48, 5, 1, 49, 1, 47, 19048, 11, 10, 10, 47, 39, 1799, 0, 48, 40, 0, 45, 48, 55, 1, 47, 23308, 12, 10, 10, 47, 39, 1799, 3, 48, 0, 0, 49, 63, 0, 42, 50, 48, 1, 49, 1, 47, 12645, 12, 10, 10, 47, 39, 1799, 4, 2, 54, 2209, 3, 2, 4, 60, 0, 1, 47, 19756, 11, 10, 10, 47, 39, 1799, 1, 47, 37145, 22, 47, 10, 47, 41, 47, 2200, 1, 47, 154, 12, 10, 10, 47, 39, 1799, 1, 47, 4116, 11, 10, 10, 47, 39, 1799, 1, 10, 5482, 3, 5, 0, 0, 6, 78, 0, 0, 7, 43, 0, 1, 47, 11946, 11, 10, 10, 47, 39, 1799, 4, 5, 0, 6, 79, 0, 5, 7, 6, 0, 8, 1, 0, 5, 9, 8, 0, 10, 80, 0, 27, 11, 7, 10, 40, 11, 2537, 0, 12, 81, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 2275, 39, 2725, 0, 14, 82, 0, 11, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 2295, 39, 2537, 0, 17, 83, 0, 11, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 2315, 39, 2762, 0, 20, 84, 0, 12, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 2335, 39, 2615, 0, 23, 85, 0, 11, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 2355, 39, 2576, 0, 26, 86, 0, 12, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 2375, 39, 2604, 0, 29, 87, 0, 11, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 2395, 39, 2734, 0, 32, 88, 0, 12, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 2415, 39, 2652, 0, 35, 89, 0, 11, 36, 9, 35, 5, 9, 36, 26, 37, 7, 9, 40, 37, 2435, 39, 2609, 0, 38, 90, 0, 11, 39, 9, 38, 5, 9, 39, 26, 40, 7, 9, 40, 40, 2455, 39, 2790, 0, 41, 91, 0, 12, 42, 9, 41, 5, 9, 42, 26, 43, 7, 9, 40, 43, 2475, 39, 2548, 0, 44, 92, 0, 12, 45, 9, 44, 5, 9, 45, 26, 46, 7, 9, 40, 46, 2495, 39, 2680, 0, 47, 93, 0, 11, 48, 9, 47, 5, 9, 48, 26, 49, 7, 9, 40, 49, 2515, 39, 2628, 0, 50, 94, 0, 12, 51, 9, 50, 5, 9, 51, 26, 52, 7, 9, 40, 52, 2535, 39, 2701, 39, 2248, 58, 3, 1, 53, 2756, 12, 7, 7, 53, 39, 2248, 4, 2, 59, 2628, 3, 4, 2604, 41, 0, 2567, 1, 53, 3537, 12, 7, 7, 53, 39, 2248, 1, 53, 20946, 11, 7, 7, 53, 39, 2248, 1, 53, 26203, 22, 53, 7, 53, 41, 53, 2595, 1, 53, 2323, 12, 7, 7, 53, 39, 2248, 1, 53, 3645, 12, 7, 7, 53, 39, 2248, 1, 7, 6466, 46, 4, 0, 54, 40, 0, 45, 54, 55, 1, 3, 2701, 1, 53, 24022, 11, 7, 7, 53, 39, 2248, 1, 7, 56230, 3, 54, 0, 0, 55, 95, 0, 42, 56, 54, 1, 55, 1, 53, 47008, 12, 7, 7, 53, 39, 2248, 1, 53, 22348, 22, 53, 7, 53, 41, 53, 2671, 1, 53, 12865, 12, 7, 7, 53, 39, 2248, 1, 53, 12877, 11, 7, 7, 53, 39, 2248, 3, 54, 0, 0, 55, 59, 0, 42, 56, 54, 1, 55, 1, 53, 943, 11, 7, 7, 53, 39, 2248, 1, 7, 15407, 3, 54, 0, 0, 55, 63, 0, 42, 56, 54, 1, 55, 1, 53, 18123, 11, 7, 7, 53, 39, 2248, 1, 53, 1103, 11, 7, 7, 53, 39, 2248, 1, 53, 32039, 22, 53, 7, 53, 41, 53, 2753, 1, 53, 4694, 11, 7, 7, 53, 39, 2248, 1, 53, 16252, 12, 7, 7, 53, 39, 2248, 1, 53, 56008, 22, 53, 7, 53, 41, 53, 2781, 1, 53, 41122, 12, 7, 7, 53, 39, 2248, 1, 53, 41122, 12, 7, 7, 53, 39, 2248, 2, 54, 60, 0, 0, 55, 61, 0, 44, 56, 54, 1, 55, 46, 56, 4, 6, 0, 7, 96, 0, 5, 8, 7, 0, 9, 1, 0, 5, 10, 9, 0, 11, 97, 0, 27, 12, 8, 11, 40, 12, 3090, 0, 13, 98, 0, 5, 10, 13, 26, 14, 8, 10, 40, 14, 2848, 39, 3090, 0, 15, 99, 0, 11, 16, 10, 15, 5, 10, 16, 26, 17, 8, 10, 40, 17, 2868, 39, 3244, 0, 18, 100, 0, 11, 19, 10, 18, 5, 10, 19, 26, 20, 8, 10, 40, 20, 2888, 39, 3268, 0, 21, 101, 0, 12, 22, 10, 21, 5, 10, 22, 26, 23, 8, 10, 40, 23, 2908, 39, 3286, 0, 24, 102, 0, 12, 25, 10, 24, 5, 10, 25, 26, 26, 8, 10, 40, 26, 2928, 39, 3134, 0, 27, 103, 0, 11, 28, 10, 27, 5, 10, 28, 26, 29, 8, 10, 40, 29, 2948, 39, 3167, 0, 30, 104, 0, 12, 31, 10, 30, 5, 10, 31, 26, 32, 8, 10, 40, 32, 2968, 39, 3277, 0, 33, 105, 0, 11, 34, 10, 33, 5, 10, 34, 26, 35, 8, 10, 40, 35, 2988, 39, 3198, 0, 36, 106, 0, 11, 37, 10, 36, 5, 10, 37, 26, 38, 8, 10, 40, 38, 3008, 39, 3216, 0, 39, 107, 0, 12, 40, 10, 39, 5, 10, 40, 26, 41, 8, 10, 40, 41, 3028, 39, 3162, 0, 42, 108, 0, 11, 43, 10, 42, 5, 10, 43, 26, 44, 8, 10, 40, 44, 3048, 39, 3203, 0, 45, 109, 0, 12, 46, 10, 45, 5, 10, 46, 26, 47, 8, 10, 40, 47, 3068, 39, 3106, 0, 48, 110, 0, 11, 49, 10, 48, 5, 10, 49, 26, 50, 8, 10, 40, 50, 3088, 39, 3178, 39, 2821, 5, 4, 5, 1, 3, 3162, 55, 1, 51, 20813, 11, 8, 8, 51, 39, 2821, 1, 51, 2514, 22, 51, 8, 51, 41, 51, 3125, 1, 51, 37061, 11, 8, 8, 51, 39, 2821, 1, 51, 37061, 11, 8, 8, 51, 39, 2821, 1, 51, 42042, 22, 51, 8, 51, 41, 51, 3153, 1, 51, 28060, 11, 8, 8, 51, 39, 2821, 1, 51, 6824, 11, 8, 8, 51, 39, 2821, 1, 8, 39235, 45, 4, 58, 3, 1, 51, 9930, 12, 8, 8, 51, 39, 2821, 4, 2, 59, 3244, 3, 4, 3198, 0, 5, 111, 0, 1, 51, 46915, 12, 8, 8, 51, 39, 2821, 1, 8, 15872, 46, 4, 55, 1, 3, 3277, 1, 51, 21746, 12, 8, 8, 51, 39, 2821, 1, 51, 53368, 22, 51, 8, 51, 41, 51, 3235, 1, 51, 21254, 12, 8, 8, 51, 39, 2821, 1, 51, 1509, 11, 8, 8, 51, 39, 2821, 1, 8, 37108, 3, 52, 0, 0, 53, 95, 0, 42, 54, 52, 1, 53, 1, 51, 11306, 12, 8, 8, 51, 39, 2821, 1, 51, 33107, 12, 8, 8, 51, 39, 2821, 1, 8, 7340, 0, 52, 40, 0, 45, 52, 1, 51, 33447, 22, 51, 8, 51, 41, 51, 3305, 1, 51, 741, 12, 8, 8, 51, 39, 2821, 1, 51, 23234, 11, 8, 8, 51, 39, 2821, 4, 8, 0, 9, 112, 0, 5, 10, 9, 0, 11, 1, 0, 5, 12, 11, 0, 13, 113, 0, 27, 14, 10, 13, 40, 14, 3659, 0, 15, 114, 0, 5, 12, 15, 26, 16, 10, 12, 40, 16, 3357, 39, 3814, 0, 17, 115, 0, 12, 18, 12, 17, 5, 12, 18, 26, 19, 10, 12, 40, 19, 3377, 39, 3842, 0, 20, 116, 0, 11, 21, 12, 20, 5, 12, 21, 26, 22, 10, 12, 40, 22, 3397, 39, 3895, 0, 23, 117, 0, 12, 24, 12, 23, 5, 12, 24, 26, 25, 10, 12, 40, 25, 3417, 39, 3734, 0, 26, 118, 0, 11, 27, 12, 26, 5, 12, 27, 26, 28, 10, 12, 40, 28, 3437, 39, 3743, 0, 29, 119, 0, 12, 30, 12, 29, 5, 12, 30, 26, 31, 10, 12, 40, 31, 3457, 39, 3659, 0, 32, 120, 0, 11, 33, 12, 32, 5, 12, 33, 26, 34, 10, 12, 40, 34, 3477, 39, 3923, 0, 35, 121, 0, 11, 36, 12, 35, 5, 12, 36, 26, 37, 10, 12, 40, 37, 3497, 39, 3703, 0, 38, 122, 0, 12, 39, 12, 38, 5, 12, 39, 26, 40, 10, 12, 40, 40, 3517, 39, 3875, 0, 41, 123, 0, 11, 42, 12, 41, 5, 12, 42, 26, 43, 10, 12, 40, 43, 3537, 39, 3773, 0, 44, 124, 0, 12, 45, 12, 44, 5, 12, 45, 26, 46, 10, 12, 40, 46, 3557, 39, 3688, 0, 47, 125, 0, 11, 48, 12, 47, 5, 12, 48, 26, 49, 10, 12, 40, 49, 3577, 39, 3854, 0, 50, 126, 0, 12, 51, 12, 50, 5, 12, 51, 26, 52, 10, 12, 40, 52, 3597, 39, 3713, 0, 53, 127, 0, 12, 54, 12, 53, 5, 12, 54, 26, 55, 10, 12, 40, 55, 3617, 39, 3943, 0, 56, 128, 0, 12, 57, 12, 56, 5, 12, 57, 26, 58, 10, 12, 40, 58, 3637, 39, 3782, 0, 59, 129, 0, 12, 60, 12, 59, 5, 12, 60, 26, 61, 10, 12, 40, 61, 3657, 39, 3767, 39, 3330, 0, 63, 130, 0, 22, 64, 3, 63, 41, 64, 3679, 1, 62, 20870, 11, 10, 10, 62, 39, 3330, 1, 62, 22787, 12, 10, 10, 62, 39, 3330, 42, 63, 5, 1, 6, 55, 1, 62, 47181, 11, 10, 10, 62, 39, 3330, 55, 1, 62, 2957, 11, 10, 10, 62, 39, 3330, 3, 63, 0, 0, 64, 63, 0, 42, 65, 63, 1, 64, 1, 62, 45394, 12, 10, 10, 62, 39, 3330, 1, 62, 4401, 11, 10, 10, 62, 39, 3330, 1, 10, 39003, 3, 63, 0, 0, 64, 62, 0, 42, 65, 63, 1, 64, 1, 62, 21604, 11, 10, 10, 62, 39, 3330, 0, 63, 40, 0, 45, 63, 1, 62, 36052, 12, 10, 10, 62, 39, 3330, 54, 3743, 4, 0, 63, 35, 0, 26, 64, 3, 63, 41, 64, 3805, 1, 62, 31195, 11, 10, 10, 62, 39, 3330, 1, 62, 40700, 11, 10, 10, 62, 39, 3330, 1, 62, 28503, 22, 62, 10, 62, 41, 62, 3833, 1, 62, 17235, 11, 10, 10, 62, 39, 3330, 1, 62, 22202, 11, 10, 10, 62, 39, 3330, 5, 3, 7, 1, 62, 30755, 11, 10, 10, 62, 39, 3330, 3, 63, 0, 0, 64, 131, 0, 42, 65, 63, 1, 64, 1, 62, 14189, 12, 10, 10, 62, 39, 3330, 3, 5, 0, 0, 63, 132, 0, 11, 6, 63, 3, 1, 62, 31085, 12, 10, 10, 62, 39, 3330, 1, 62, 36691, 22, 62, 10, 62, 41, 62, 3914, 1, 62, 53708, 12, 10, 10, 62, 39, 3330, 1, 62, 18869, 12, 10, 10, 62, 39, 3330, 5, 63, 3, 0, 63, 36, 0, 11, 7, 3, 63, 1, 62, 41070, 12, 10, 10, 62, 39, 3330, 4, 2, 0, 63, 1, 0, 5, 3, 63, 1, 62, 8991, 11, 10, 10, 62, 39, 3330, 4, 13, 0, 14, 133, 0, 5, 15, 14, 0, 16, 1, 0, 5, 17, 16, 0, 18, 134, 0, 27, 19, 15, 18, 40, 19, 4406, 0, 20, 135, 0, 5, 17, 20, 26, 21, 15, 17, 40, 21, 4004, 39, 4457, 0, 22, 136, 0, 11, 23, 17, 22, 5, 17, 23, 26, 24, 15, 17, 40, 24, 4024, 39, 4759, 0, 25, 137, 0, 11, 26, 17, 25, 5, 17, 26, 26, 27, 15, 17, 40, 27, 4044, 39, 4722, 0, 28, 138, 0, 11, 29, 17, 28, 5, 17, 29, 26, 30, 15, 17, 40, 30, 4064, 39, 4532, 0, 31, 139, 0, 12, 32, 17, 31, 5, 17, 32, 26, 33, 15, 17, 40, 33, 4084, 39, 4731, 0, 34, 140, 0, 11, 35, 17, 34, 5, 17, 35, 26, 36, 15, 17, 40, 36, 4104, 39, 4491, 0, 37, 141, 0, 12, 38, 17, 37, 5, 17, 38, 26, 39, 15, 17, 40, 39, 4124, 39, 4701, 0, 40, 142, 0, 12, 41, 17, 40, 5, 17, 41, 26, 42, 15, 17, 40, 42, 4144, 39, 4520, 0, 43, 143, 0, 11, 44, 17, 43, 5, 17, 44, 26, 45, 15, 17, 40, 45, 4164, 39, 4676, 0, 46, 144, 0, 12, 47, 17, 46, 5, 17, 47, 26, 48, 15, 17, 40, 48, 4184, 39, 4590, 0, 49, 145, 0, 11, 50, 17, 49, 5, 17, 50, 26, 51, 15, 17, 40, 51, 4204, 39, 4578, 0, 52, 146, 0, 12, 53, 17, 52, 5, 17, 53, 26, 54, 15, 17, 40, 54, 4224, 39, 4406, 0, 55, 147, 0, 12, 56, 17, 55, 5, 17, 56, 26, 57, 15, 17, 40, 57, 4244, 39, 4630, 0, 58, 148, 0, 11, 59, 17, 58, 5, 17, 59, 26, 60, 15, 17, 40, 60, 4264, 39, 4658, 0, 61, 149, 0, 12, 62, 17, 61, 5, 17, 62, 26, 63, 15, 17, 40, 63, 4284, 39, 4429, 0, 64, 150, 0, 12, 65, 17, 64, 5, 17, 65, 26, 66, 15, 17, 40, 66, 4304, 39, 4754, 0, 67, 151, 0, 12, 68, 17, 67, 5, 17, 68, 26, 69, 15, 17, 40, 69, 4324, 39, 4649, 0, 70, 152, 0, 11, 71, 17, 70, 5, 17, 71, 26, 72, 15, 17, 40, 72, 4344, 39, 4610, 0, 73, 153, 0, 12, 74, 17, 73, 5, 17, 74, 26, 75, 15, 17, 40, 75, 4364, 39, 4643, 0, 76, 154, 0, 11, 77, 17, 76, 5, 17, 77, 26, 78, 15, 17, 40, 78, 4384, 39, 4685, 0, 79, 155, 0, 12, 80, 17, 79, 5, 17, 80, 26, 81, 15, 17, 40, 81, 4404, 39, 4560, 39, 3977, 1, 15, 7698, 3, 10, 0, 0, 83, 63, 0, 11, 11, 83, 3, 1, 82, 763, 12, 15, 15, 82, 39, 3977, 1, 82, 60423, 22, 82, 15, 82, 41, 82, 4448, 1, 82, 37861, 12, 15, 15, 82, 39, 3977, 1, 82, 45263, 12, 15, 15, 82, 39, 3977, 59, 4731, 4, 5, 4754, 0, 83, 36, 0, 26, 84, 3, 83, 41, 84, 4482, 1, 82, 45295, 11, 15, 15, 82, 39, 3977, 1, 82, 1607, 12, 15, 15, 82, 39, 3977, 0, 83, 156, 0, 22, 84, 3, 83, 41, 84, 4511, 1, 82, 18714, 12, 15, 15, 82, 39, 3977, 1, 82, 57526, 12, 15, 15, 82, 39, 3977, 1, 15, 7267, 1, 82, 5586, 11, 15, 15, 82, 39, 3977, 1, 82, 57646, 22, 82, 15, 82, 41, 82, 4551, 1, 82, 2419, 11, 15, 15, 82, 39, 3977, 1, 82, 41517, 12, 15, 15, 82, 39, 3977, 42, 83, 6, 1, 7, 55, 1, 4, 4406, 1, 82, 55846, 12, 15, 15, 82, 39, 3977, 5, 3, 12, 1, 82, 47885, 11, 15, 15, 82, 39, 3977, 5, 83, 3, 0, 83, 36, 0, 11, 12, 3, 83, 1, 82, 2364, 11, 15, 15, 82, 39, 3977, 3, 6, 0, 0, 83, 132, 0, 11, 7, 83, 3, 1, 82, 13299, 11, 15, 15, 82, 39, 3977, 1, 4, 4520, 55, 1, 82, 4355, 11, 15, 15, 82, 39, 3977, 0, 83, 40, 0, 45, 83, 1, 82, 25464, 11, 15, 15, 82, 39, 3977, 4, 2, 0, 83, 1, 0, 5, 3, 83, 1, 82, 134, 12, 15, 15, 82, 39, 3977, 1, 82, 43849, 12, 15, 15, 82, 39, 3977, 42, 83, 8, 1, 9, 58, 4, 1, 82, 36017, 12, 15, 15, 82, 39, 3977, 3, 83, 0, 0, 84, 157, 0, 42, 85, 83, 1, 84, 1, 82, 28662, 12, 15, 15, 82, 39, 3977, 1, 82, 43849, 11, 15, 15, 82, 39, 3977, 1, 15, 8324, 3, 8, 0, 0, 83, 158, 0, 11, 9, 83, 3, 1, 82, 56720, 11, 15, 15, 82, 39, 3977, 1, 15, 29027, 46, 5, 42, 83, 10, 1, 11, 1, 82, 5918, 11, 15, 15, 82, 39, 3977, 4, 13, 0, 14, 159, 0, 5, 15, 14, 0, 16, 1, 0, 5, 17, 16, 0, 18, 160, 0, 27, 19, 15, 18, 40, 19, 5178, 0, 20, 161, 0, 5, 17, 20, 26, 21, 15, 17, 40, 21, 4816, 39, 5481, 0, 22, 162, 0, 11, 23, 17, 22, 5, 17, 23, 26, 24, 15, 17, 40, 24, 4836, 39, 5198, 0, 25, 163, 0, 11, 26, 17, 25, 5, 17, 26, 26, 27, 15, 17, 40, 27, 4856, 39, 5508, 0, 28, 164, 0, 11, 29, 17, 28, 5, 17, 29, 26, 30, 15, 17, 40, 30, 4876, 39, 5449, 0, 31, 165, 0, 12, 32, 17, 31, 5, 17, 32, 26, 33, 15, 17, 40, 33, 4896, 39, 5469, 0, 34, 166, 0, 11, 35, 17, 34, 5, 17, 35, 26, 36, 15, 17, 40, 36, 4916, 39, 5408, 0, 37, 167, 0, 11, 38, 17, 37, 5, 17, 38, 26, 39, 15, 17, 40, 39, 4936, 39, 5351, 0, 40, 168, 0, 12, 41, 17, 40, 5, 17, 41, 26, 42, 15, 17, 40, 42, 4956, 39, 5261, 0, 43, 169, 0, 11, 44, 17, 43, 5, 17, 44, 26, 45, 15, 17, 40, 45, 4976, 39, 5323, 0, 46, 170, 0, 11, 47, 17, 46, 5, 17, 47, 26, 48, 15, 17, 40, 48, 4996, 39, 5513, 0, 49, 171, 0, 12, 50, 17, 49, 5, 17, 50, 26, 51, 15, 17, 40, 51, 5016, 39, 5385, 0, 52, 172, 0, 11, 53, 17, 52, 5, 17, 53, 26, 54, 15, 17, 40, 54, 5036, 39, 5284, 0, 55, 173, 0, 12, 56, 17, 55, 5, 17, 56, 26, 57, 15, 17, 40, 57, 5056, 39, 5219, 0, 58, 174, 0, 12, 59, 17, 58, 5, 17, 59, 26, 60, 15, 17, 40, 60, 5076, 39, 5420, 0, 61, 175, 0, 11, 62, 17, 61, 5, 17, 62, 26, 63, 15, 17, 40, 63, 5096, 39, 5178, 0, 64, 176, 0, 12, 65, 17, 64, 5, 17, 65, 26, 66, 15, 17, 40, 66, 5116, 39, 5494, 0, 67, 177, 0, 11, 68, 17, 67, 5, 17, 68, 26, 69, 15, 17, 40, 69, 5136, 39, 5225, 0, 70, 178, 0, 12, 71, 17, 70, 5, 17, 71, 26, 72, 15, 17, 40, 72, 5156, 39, 5241, 0, 73, 179, 0, 12, 74, 17, 73, 5, 17, 74, 26, 75, 15, 17, 40, 75, 5176, 39, 5302, 39, 4789, 0, 77, 35, 0, 11, 78, 3, 77, 5, 12, 78, 1, 76, 3440, 11, 15, 15, 76, 39, 4789, 3, 77, 0, 0, 78, 157, 0, 42, 79, 77, 1, 78, 1, 76, 7848, 12, 15, 15, 76, 39, 4789, 0, 77, 40, 0, 45, 77, 42, 77, 8, 1, 9, 58, 4, 1, 76, 5662, 12, 15, 15, 76, 39, 4789, 0, 77, 36, 0, 11, 78, 3, 77, 5, 12, 78, 1, 76, 8404, 12, 15, 15, 76, 39, 4789, 1, 15, 26363, 3, 10, 0, 0, 77, 63, 0, 11, 11, 77, 3, 1, 76, 11474, 12, 15, 15, 76, 39, 4789, 4, 2, 0, 77, 1, 0, 5, 3, 77, 1, 76, 45406, 12, 15, 15, 76, 39, 4789, 41, 0, 5314, 1, 76, 4486, 11, 15, 15, 76, 39, 4789, 1, 76, 7358, 12, 15, 15, 76, 39, 4789, 1, 76, 19734, 22, 76, 15, 76, 41, 76, 5342, 1, 76, 38845, 12, 15, 15, 76, 39, 4789, 1, 76, 12804, 11, 15, 15, 76, 39, 4789, 59, 5385, 4, 5, 5508, 0, 77, 36, 0, 26, 78, 3, 77, 41, 78, 5376, 1, 76, 18243, 11, 15, 15, 76, 39, 4789, 1, 76, 22175, 12, 15, 15, 76, 39, 4789, 1, 15, 46855, 3, 8, 0, 0, 77, 158, 0, 11, 9, 77, 3, 1, 76, 11689, 11, 15, 15, 76, 39, 4789, 1, 15, 34151, 1, 76, 9592, 12, 15, 15, 76, 39, 4789, 0, 77, 180, 0, 22, 78, 3, 77, 41, 78, 5440, 1, 76, 19694, 11, 15, 15, 76, 39, 4789, 1, 76, 41861, 11, 15, 15, 76, 39, 4789, 3, 6, 0, 0, 77, 132, 0, 11, 7, 77, 3, 1, 76, 8455, 12, 15, 15, 76, 39, 4789, 5, 3, 12, 1, 76, 18222, 12, 15, 15, 76, 39, 4789, 1, 4, 5408, 55, 1, 76, 24750, 11, 15, 15, 76, 39, 4789, 42, 77, 10, 1, 11, 1, 76, 9670, 11, 15, 15, 76, 39, 4789, 1, 15, 52882, 46, 5, 42, 77, 6, 1, 7, 55, 1, 4, 5261, 1, 76, 7213, 12, 15, 15, 76, 39, 4789]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 137; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms-cff/source.js b/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms-cff/source.js new file mode 100644 index 00000000..87c96bf9 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms-cff/source.js @@ -0,0 +1,19 @@ +var log = []; +function note(value) { log.push(value); } +function normalEnd(fail) { try { if (fail) throw new Error("x"); note("try"); } catch (e) { note("catch"); } note("after"); } +function blockAlwaysThrows(fail) { try { throw new Error("always"); } catch (e) { note("caught " + e.message); } note("after"); } +function finallyNormal(fail) { try { if (fail) throw new Error("x"); note("try"); } finally { note("finally"); } note("after"); } +function finallyReturns(fail) { try { return "returned"; } finally { note("finally"); } } +function breakInTryInLoop(fail) { for (var i = 0; i < 4; i++) { try { if (i === 2) break; note("i" + i); } catch (e) { note("catch"); } note("next"); } note("after"); } +function continueThroughFinally(fail) { for (var i = 0; i < 3; i++) { try { if (i === 1) continue; note("i" + i); } finally { note("f" + i); } note("after" + i); } note("done"); } +function continueToBranchingUpdate(fail) { for (var i = 0; i < 6; i = fail ? i + 2 : i + 1) { try { if (i === 1) continue; note("i" + i); } finally { note("f" + i); } note("after" + i); } note("done"); } +var forms = [normalEnd, blockAlwaysThrows, finallyNormal, finallyReturns, breakInTryInLoop, continueThroughFinally, continueToBranchingUpdate]; +window.TEST_OUTPUT = []; +for (var index = 0; index < forms.length; index++) { + for (var k = 0; k < 2; k++) { + log = []; + var outcome; + try { outcome = ["value", forms[index](k === 1)]; } catch (error) { outcome = ["thrown", error.message]; } + window.TEST_OUTPUT.push([index, k, outcome, log]); + } +} diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms/encoded.js b/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms/encoded.js new file mode 100644 index 00000000..c5eabf65 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */"length", /* 4 */2, /* 5 */undefined, /* 6 */"value", /* 7 */1, /* 8 */"thrown", /* 9 */"message", /* 10 */"push", /* 11 */"Error", /* 12 */"x", /* 13 */"try", /* 14 */"catch", /* 15 */"after", /* 16 */"always", /* 17 */"caught ", /* 18 */"finally", /* 19 */"returned", /* 20 */4, /* 21 */"i", /* 22 */"next", /* 23 */3, /* 24 */"f", /* 25 */"done", /* 26 */6]; +var BYTECODE = [4, 1, 47, 16, 309, 1, 6, 1, 0, 1, 2, 5, 3, 16, 47, 16, 334, 1, 7, 1, 0, 1, 3, 5, 4, 16, 47, 16, 402, 1, 8, 1, 0, 1, 3, 5, 5, 16, 47, 16, 467, 1, 8, 1, 0, 1, 3, 5, 6, 16, 47, 16, 544, 1, 8, 1, 0, 1, 3, 5, 7, 16, 47, 16, 594, 1, 8, 1, 0, 1, 3, 5, 8, 16, 47, 16, 708, 1, 9, 1, 0, 1, 3, 5, 9, 16, 47, 16, 844, 1, 9, 1, 0, 1, 3, 5, 10, 16, 48, 16, 0, 5, 2, 16, 48, 16, 7, 4, 5, 6, 7, 8, 9, 10, 5, 11, 16, 2, 16, 0, 0, 0, 17, 1, 0, 48, 18, 0, 9, 16, 17, 18, 0, 16, 2, 0, 5, 12, 16, 0, 16, 3, 0, 8, 17, 11, 16, 22, 16, 12, 17, 40, 16, 303, 0, 16, 2, 0, 5, 13, 16, 0, 16, 4, 0, 22, 17, 13, 16, 40, 17, 287, 48, 16, 0, 5, 2, 16, 0, 14, 5, 0, 54, 218, 15, 0, 16, 6, 0, 8, 17, 11, 12, 0, 18, 7, 0, 26, 19, 13, 18, 43, 18, 11, 17, 1, 19, 48, 17, 2, 16, 18, 5, 14, 17, 55, 39, 238, 0, 16, 8, 0, 0, 17, 9, 0, 8, 18, 15, 17, 48, 17, 2, 16, 18, 5, 14, 17, 2, 16, 0, 0, 0, 17, 1, 0, 8, 18, 16, 17, 0, 16, 10, 0, 8, 17, 18, 16, 48, 16, 4, 12, 13, 14, 2, 43, 19, 18, 17, 1, 16, 5, 16, 13, 0, 16, 7, 0, 11, 17, 13, 16, 5, 13, 17, 39, 161, 5, 16, 12, 0, 16, 7, 0, 11, 17, 12, 16, 5, 12, 17, 39, 139, 0, 16, 5, 0, 45, 16, 4, 2, 3, 3, 0, 0, 4, 10, 0, 8, 5, 3, 4, 43, 4, 3, 5, 1, 0, 0, 3, 5, 0, 45, 3, 4, 2, 54, 372, 3, 40, 0, 357, 2, 4, 11, 0, 0, 5, 12, 0, 44, 6, 4, 1, 5, 46, 6, 3, 4, 0, 0, 5, 13, 0, 42, 6, 4, 1, 5, 55, 39, 384, 3, 4, 0, 0, 5, 14, 0, 42, 6, 4, 1, 5, 3, 4, 0, 0, 5, 15, 0, 42, 6, 4, 1, 5, 0, 4, 5, 0, 45, 4, 4, 2, 54, 425, 3, 2, 4, 11, 0, 0, 5, 16, 0, 44, 6, 4, 1, 5, 46, 6, 55, 39, 449, 3, 4, 0, 0, 5, 17, 0, 0, 6, 9, 0, 8, 7, 3, 6, 11, 6, 5, 7, 42, 5, 4, 1, 6, 3, 4, 0, 0, 5, 15, 0, 42, 6, 4, 1, 5, 0, 4, 5, 0, 45, 4, 4, 2, 59, 510, 3, 4, 524, 40, 0, 492, 2, 5, 11, 0, 0, 6, 12, 0, 44, 7, 5, 1, 6, 46, 7, 3, 5, 0, 0, 6, 13, 0, 42, 7, 5, 1, 6, 55, 1, 3, 526, 39, 510, 3, 5, 0, 0, 6, 18, 0, 42, 7, 5, 1, 6, 58, 3, 46, 4, 3, 5, 0, 0, 6, 15, 0, 42, 7, 5, 1, 6, 0, 5, 5, 0, 45, 5, 4, 2, 59, 570, 3, 4, 584, 0, 5, 19, 0, 5, 4, 5, 1, 3, 586, 55, 39, 570, 55, 1, 3, 588, 39, 570, 3, 5, 0, 0, 6, 18, 0, 42, 7, 5, 1, 6, 58, 3, 46, 4, 45, 4, 0, 5, 5, 0, 45, 5, 4, 2, 0, 5, 2, 0, 5, 3, 5, 0, 5, 20, 0, 22, 6, 3, 5, 40, 6, 690, 54, 650, 4, 0, 5, 4, 0, 26, 6, 3, 5, 40, 6, 631, 55, 39, 690, 3, 5, 0, 0, 6, 21, 0, 11, 7, 6, 3, 42, 6, 5, 1, 7, 55, 39, 662, 3, 5, 0, 0, 6, 14, 0, 42, 7, 5, 1, 6, 3, 5, 0, 0, 6, 22, 0, 42, 7, 5, 1, 6, 5, 5, 3, 0, 5, 7, 0, 11, 6, 3, 5, 5, 3, 6, 39, 603, 3, 5, 0, 0, 6, 15, 0, 42, 7, 5, 1, 6, 0, 5, 5, 0, 45, 5, 4, 2, 0, 6, 2, 0, 5, 3, 6, 0, 6, 23, 0, 22, 7, 3, 6, 40, 7, 826, 59, 772, 4, 5, 790, 0, 6, 7, 0, 26, 7, 3, 6, 40, 7, 750, 1, 4, 792, 55, 39, 772, 3, 6, 0, 0, 7, 21, 0, 11, 8, 7, 3, 42, 7, 6, 1, 8, 55, 1, 4, 794, 39, 772, 3, 6, 0, 0, 7, 24, 0, 11, 8, 7, 3, 42, 7, 6, 1, 8, 58, 4, 46, 5, 39, 810, 3, 6, 0, 0, 7, 15, 0, 11, 8, 7, 3, 42, 7, 6, 1, 8, 5, 6, 3, 0, 6, 7, 0, 11, 7, 3, 6, 5, 3, 7, 39, 717, 3, 6, 0, 0, 7, 25, 0, 42, 8, 6, 1, 7, 0, 6, 5, 0, 45, 6, 4, 2, 0, 6, 2, 0, 5, 3, 6, 0, 6, 26, 0, 22, 7, 3, 6, 40, 7, 978, 59, 908, 4, 5, 926, 0, 6, 7, 0, 26, 7, 3, 6, 40, 7, 886, 1, 4, 928, 55, 39, 908, 3, 6, 0, 0, 7, 21, 0, 11, 8, 7, 3, 42, 7, 6, 1, 8, 55, 1, 4, 930, 39, 908, 3, 6, 0, 0, 7, 24, 0, 11, 8, 7, 3, 42, 7, 6, 1, 8, 58, 4, 46, 5, 39, 946, 3, 6, 0, 0, 7, 15, 0, 11, 8, 7, 3, 42, 7, 6, 1, 8, 40, 0, 962, 0, 6, 4, 0, 11, 7, 3, 6, 5, 6, 7, 39, 973, 0, 7, 7, 0, 11, 8, 3, 7, 5, 6, 8, 5, 3, 6, 39, 853, 3, 6, 0, 0, 7, 25, 0, 42, 8, 6, 1, 7, 0, 6, 5, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 20; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms/source.js b/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms/source.js new file mode 100644 index 00000000..87c96bf9 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/forms/source.js @@ -0,0 +1,19 @@ +var log = []; +function note(value) { log.push(value); } +function normalEnd(fail) { try { if (fail) throw new Error("x"); note("try"); } catch (e) { note("catch"); } note("after"); } +function blockAlwaysThrows(fail) { try { throw new Error("always"); } catch (e) { note("caught " + e.message); } note("after"); } +function finallyNormal(fail) { try { if (fail) throw new Error("x"); note("try"); } finally { note("finally"); } note("after"); } +function finallyReturns(fail) { try { return "returned"; } finally { note("finally"); } } +function breakInTryInLoop(fail) { for (var i = 0; i < 4; i++) { try { if (i === 2) break; note("i" + i); } catch (e) { note("catch"); } note("next"); } note("after"); } +function continueThroughFinally(fail) { for (var i = 0; i < 3; i++) { try { if (i === 1) continue; note("i" + i); } finally { note("f" + i); } note("after" + i); } note("done"); } +function continueToBranchingUpdate(fail) { for (var i = 0; i < 6; i = fail ? i + 2 : i + 1) { try { if (i === 1) continue; note("i" + i); } finally { note("f" + i); } note("after" + i); } note("done"); } +var forms = [normalEnd, blockAlwaysThrows, finallyNormal, finallyReturns, breakInTryInLoop, continueThroughFinally, continueToBranchingUpdate]; +window.TEST_OUTPUT = []; +for (var index = 0; index < forms.length; index++) { + for (var k = 0; k < 2; k++) { + log = []; + var outcome; + try { outcome = ["value", forms[index](k === 1)]; } catch (error) { outcome = ["thrown", error.message]; } + window.TEST_OUTPUT.push([index, k, outcome, log]); + } +} diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/generation.json b/test/vm/switch/fixtures/vm-switch-statements/try-ends/generation.json new file mode 100644 index 00000000..f318d103 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/generation.json @@ -0,0 +1,29 @@ +{ + "note": "Each cell's encoded.js is the VM encoder's output for its source.js, with the leading disassembly comment block removed (the committed corpus format). Default options draw no randomness; a cell with options names them and the seed of the PRNG that replaced Math.random for the encode (mulberry32: state += 0x6d2b79f5, then the standard mulberry32 output). Regenerating with the same encoder revision, options and seed reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5", + "options": {} + }, + "cells": { + "forms": "one function per way a try statement ends: a catch that completes either way, a try block that always throws, a finally that falls through, a finally whose block returns, a break across a catch in a loop, a continue routed through a finally with code after the try, and the same continue into an update that branches; each run without and with an exception", + "dowhile-continue": "a continue routed through a finally inside a do-while, with code after the try: the continue enters the loop's test, so the loop recovers as a do-while whose native continue runs it", + "forms-cff": "the forms source with control flow flattening: the block order is the encoder's shuffle, with each catch placed elsewhere; every pop is still followed by the jump the frontend keeps", + "switch-break": "a break out of a switch across a catch that completes normally: the try has two exit tails, the break's to the switch's end and the normal one, which the catch's own completion reaches first", + "switch-break-cff": "the switch-break source with control flow flattening, a seed whose shuffle places the switch's end before the try's normal end" + }, + "cellOptions": { + "forms-cff": { + "options": { + "controlFlowFlattening": true + }, + "seed": 324508639 + }, + "switch-break-cff": { + "options": { + "controlFlowFlattening": true + }, + "seed": 3 + } + } +} diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break-cff/encoded.js b/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break-cff/encoded.js new file mode 100644 index 00000000..44f8cfb5 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break-cff/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */2, /* 4 */9, /* 5 */undefined, /* 6 */47200, /* 7 */0, /* 8 */4910, /* 9 */46778, /* 10 */422, /* 11 */17303, /* 12 */27364, /* 13 */10241, /* 14 */4478, /* 15 */14646, /* 16 */"Error", /* 17 */"x", /* 18 */3, /* 19 */31346, /* 20 */57720, /* 21 */34795, /* 22 */12197, /* 23 */22101, /* 24 */14140, /* 25 */26003, /* 26 */7584, /* 27 */25850, /* 28 */33649, /* 29 */25759, /* 30 */15459, /* 31 */10634, /* 32 */2182, /* 33 */14118, /* 34 */20185, /* 35 */27618, /* 36 */28827, /* 37 */23507, /* 38 */28997, /* 39 */12451, /* 40 */15437, /* 41 */16827, /* 42 */24304, /* 43 */18875, /* 44 */19157, /* 45 */26463, /* 46 */5710, /* 47 */"push", /* 48 */6, /* 49 */7, /* 50 */4, /* 51 */5]; +var BYTECODE = [4, 1, 47, 4, 75, 1, 34, 0, 0, 5, 2, 4, 47, 4, 347, 1, 104, 1, 0, 1, 2, 5, 3, 4, 2, 4, 0, 0, 0, 5, 1, 0, 0, 6, 2, 0, 42, 7, 3, 1, 6, 0, 6, 3, 0, 42, 8, 3, 1, 6, 0, 6, 4, 0, 42, 9, 3, 1, 6, 48, 6, 3, 7, 8, 9, 9, 4, 5, 6, 0, 4, 5, 0, 45, 4, 4, 3, 0, 4, 6, 0, 5, 5, 4, 0, 6, 7, 0, 5, 7, 6, 0, 8, 8, 0, 27, 9, 5, 8, 40, 9, 240, 0, 10, 9, 0, 5, 7, 10, 26, 11, 5, 7, 40, 11, 118, 39, 301, 0, 12, 10, 0, 11, 13, 7, 12, 5, 7, 13, 26, 14, 5, 7, 40, 14, 138, 39, 264, 0, 15, 11, 0, 12, 16, 7, 15, 5, 7, 16, 26, 17, 5, 7, 40, 17, 158, 39, 295, 0, 18, 12, 0, 12, 19, 7, 18, 5, 7, 19, 26, 20, 5, 7, 40, 20, 178, 39, 249, 0, 21, 13, 0, 11, 22, 7, 21, 5, 7, 22, 26, 23, 5, 7, 40, 23, 198, 39, 240, 0, 24, 14, 0, 11, 25, 7, 24, 5, 7, 25, 26, 26, 5, 7, 40, 26, 218, 39, 338, 0, 27, 15, 0, 11, 28, 7, 27, 5, 7, 28, 26, 29, 5, 7, 40, 29, 238, 39, 329, 39, 91, 1, 30, 4478, 11, 5, 5, 30, 39, 91, 2, 31, 16, 0, 0, 32, 17, 0, 44, 33, 31, 1, 32, 46, 33, 4, 2, 0, 31, 18, 0, 26, 32, 0, 31, 41, 32, 286, 1, 30, 17303, 12, 5, 5, 30, 39, 91, 1, 30, 44667, 12, 5, 5, 30, 39, 91, 0, 31, 5, 0, 45, 31, 1, 30, 27563, 22, 30, 5, 30, 41, 30, 320, 1, 30, 14880, 12, 5, 5, 30, 39, 91, 1, 30, 44245, 12, 5, 5, 30, 39, 91, 1, 30, 0, 11, 5, 5, 30, 39, 91, 1, 30, 4478, 12, 5, 5, 30, 39, 91, 4, 17, 0, 18, 19, 0, 5, 19, 18, 0, 20, 7, 0, 5, 21, 20, 0, 22, 20, 0, 27, 23, 19, 22, 40, 23, 892, 0, 24, 21, 0, 5, 21, 24, 26, 25, 19, 21, 40, 25, 390, 39, 1257, 0, 26, 22, 0, 12, 27, 21, 26, 5, 21, 27, 26, 28, 19, 21, 40, 28, 410, 39, 1278, 0, 29, 23, 0, 11, 30, 21, 29, 5, 21, 30, 26, 31, 19, 21, 40, 31, 430, 39, 1208, 0, 32, 24, 0, 12, 33, 21, 32, 5, 21, 33, 26, 34, 19, 21, 40, 34, 450, 39, 1102, 0, 35, 25, 0, 11, 36, 21, 35, 5, 21, 36, 26, 37, 19, 21, 40, 37, 470, 39, 1248, 0, 38, 26, 0, 11, 39, 21, 38, 5, 21, 39, 26, 40, 19, 21, 40, 40, 490, 39, 1041, 0, 41, 27, 0, 12, 42, 21, 41, 5, 21, 42, 26, 43, 19, 21, 40, 43, 510, 39, 1073, 0, 44, 28, 0, 12, 45, 21, 44, 5, 21, 45, 26, 46, 19, 21, 40, 46, 530, 39, 1020, 0, 47, 29, 0, 11, 48, 21, 47, 5, 21, 48, 26, 49, 19, 21, 40, 49, 550, 39, 1155, 0, 50, 30, 0, 11, 51, 21, 50, 5, 21, 51, 26, 52, 19, 21, 40, 52, 570, 39, 927, 0, 53, 31, 0, 12, 54, 21, 53, 5, 21, 54, 26, 55, 19, 21, 40, 55, 590, 39, 1093, 0, 56, 32, 0, 11, 57, 21, 56, 5, 21, 57, 26, 58, 19, 21, 40, 58, 610, 39, 1126, 0, 59, 33, 0, 11, 60, 21, 59, 5, 21, 60, 26, 61, 19, 21, 40, 61, 630, 39, 1117, 0, 62, 34, 0, 12, 63, 21, 62, 5, 21, 63, 26, 64, 19, 21, 40, 64, 650, 39, 1176, 0, 65, 35, 0, 12, 66, 21, 65, 5, 21, 66, 26, 67, 19, 21, 40, 67, 670, 39, 963, 0, 68, 36, 0, 11, 69, 21, 68, 5, 21, 69, 26, 70, 19, 21, 40, 70, 690, 39, 921, 0, 71, 37, 0, 11, 72, 21, 71, 5, 21, 72, 26, 73, 19, 21, 40, 73, 710, 39, 1217, 0, 74, 38, 0, 12, 75, 21, 74, 5, 21, 75, 26, 76, 19, 21, 40, 76, 730, 39, 942, 0, 77, 39, 0, 11, 78, 21, 77, 5, 21, 78, 26, 79, 19, 21, 40, 79, 750, 39, 991, 0, 80, 40, 0, 11, 81, 21, 80, 5, 21, 81, 26, 82, 19, 21, 40, 82, 770, 39, 1315, 0, 83, 41, 0, 12, 84, 21, 83, 5, 21, 84, 26, 85, 19, 21, 40, 85, 790, 39, 1336, 0, 86, 42, 0, 11, 87, 21, 86, 5, 21, 87, 26, 88, 19, 21, 40, 88, 810, 39, 1287, 0, 89, 43, 0, 12, 90, 21, 89, 5, 21, 90, 26, 91, 19, 21, 40, 91, 830, 39, 1233, 0, 92, 44, 0, 12, 93, 21, 92, 5, 21, 93, 26, 94, 19, 21, 40, 94, 850, 39, 1049, 0, 95, 45, 0, 11, 96, 21, 95, 5, 21, 96, 26, 97, 19, 21, 40, 97, 870, 39, 892, 0, 98, 46, 0, 12, 99, 21, 98, 5, 21, 99, 26, 100, 19, 21, 40, 100, 890, 39, 1193, 39, 363, 0, 102, 2, 0, 26, 103, 0, 102, 41, 103, 912, 1, 101, 46214, 12, 19, 19, 101, 39, 363, 1, 101, 12735, 12, 19, 19, 101, 39, 363, 0, 102, 5, 0, 45, 102, 43, 102, 3, 11, 1, 12, 1, 101, 15459, 12, 19, 19, 101, 39, 363, 0, 102, 47, 0, 8, 13, 3, 102, 0, 14, 48, 0, 1, 101, 3494, 11, 19, 19, 101, 39, 363, 1, 101, 54245, 22, 101, 19, 101, 41, 101, 982, 1, 101, 40971, 11, 19, 19, 101, 39, 363, 1, 101, 52834, 11, 19, 19, 101, 39, 363, 0, 102, 3, 0, 26, 103, 0, 102, 41, 103, 1011, 1, 101, 12015, 11, 19, 19, 101, 39, 363, 1, 101, 5183, 11, 19, 19, 101, 39, 363, 0, 102, 47, 0, 8, 5, 3, 102, 0, 6, 3, 0, 1, 101, 51415, 11, 19, 19, 101, 39, 363, 43, 102, 3, 15, 1, 16, 45, 3, 1, 19, 24398, 0, 102, 47, 0, 8, 7, 3, 102, 0, 8, 18, 0, 1, 101, 19157, 11, 19, 19, 101, 39, 363, 54, 1049, 4, 3, 102, 0, 42, 103, 102, 1, 0, 1, 101, 12565, 11, 19, 19, 101, 39, 363, 1, 101, 4285, 11, 19, 19, 101, 39, 363, 43, 102, 3, 13, 1, 14, 1, 101, 153, 12, 19, 19, 101, 39, 363, 1, 101, 24466, 12, 19, 19, 101, 39, 363, 0, 102, 2, 0, 26, 103, 0, 102, 41, 103, 1146, 1, 101, 2103, 11, 19, 19, 101, 39, 363, 1, 101, 19149, 11, 19, 19, 101, 39, 363, 0, 102, 47, 0, 8, 15, 3, 102, 0, 16, 49, 0, 1, 101, 33740, 11, 19, 19, 101, 39, 363, 4, 2, 48, 102, 0, 5, 3, 102, 1, 101, 6067, 11, 19, 19, 101, 39, 363, 43, 102, 3, 9, 1, 10, 1, 101, 9802, 11, 19, 19, 101, 39, 363, 1, 101, 10254, 11, 19, 19, 101, 39, 363, 43, 102, 3, 5, 1, 6, 55, 1, 101, 21267, 12, 19, 19, 101, 39, 363, 43, 102, 3, 7, 1, 8, 1, 101, 8760, 12, 19, 19, 101, 39, 363, 1, 101, 18266, 12, 19, 19, 101, 39, 363, 0, 102, 47, 0, 8, 9, 3, 102, 0, 10, 50, 0, 1, 101, 10356, 11, 19, 19, 101, 39, 363, 1, 101, 8748, 11, 19, 19, 101, 39, 363, 1, 101, 37849, 22, 101, 19, 101, 41, 101, 1306, 1, 101, 6368, 12, 19, 19, 101, 39, 363, 1, 101, 38032, 12, 19, 19, 101, 39, 363, 0, 102, 47, 0, 8, 11, 3, 102, 0, 12, 51, 0, 1, 101, 9088, 12, 19, 19, 101, 39, 363, 55, 1, 101, 7720, 12, 19, 19, 101, 39, 363]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break-cff/source.js b/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break-cff/source.js new file mode 100644 index 00000000..88a71901 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break-cff/source.js @@ -0,0 +1,3 @@ +function g(i) { if (i === 3) throw new Error("x"); } +function f(x) { var out = []; switch (x) { case 1: try { g(x); if (x === 1) break; out.push(2); } catch (e) { out.push(3); } out.push(4); case 2: out.push(5); break; default: out.push(6); } out.push(7); return out; } +window.TEST_OUTPUT = [f(1), f(2), f(9)]; diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break/encoded.js b/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break/encoded.js new file mode 100644 index 00000000..538eb7e4 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */2, /* 4 */9, /* 5 */undefined, /* 6 */3, /* 7 */"Error", /* 8 */"x", /* 9 */"push", /* 10 */4, /* 11 */5, /* 12 */6, /* 13 */7]; +var BYTECODE = [4, 1, 47, 4, 75, 1, 6, 0, 0, 5, 2, 4, 47, 4, 109, 1, 8, 1, 0, 1, 2, 5, 3, 4, 2, 4, 0, 0, 0, 5, 1, 0, 0, 6, 2, 0, 42, 7, 3, 1, 6, 0, 6, 3, 0, 42, 8, 3, 1, 6, 0, 6, 4, 0, 42, 9, 3, 1, 6, 48, 6, 3, 7, 8, 9, 9, 4, 5, 6, 0, 4, 5, 0, 45, 4, 4, 2, 0, 3, 6, 0, 26, 4, 0, 3, 40, 4, 103, 2, 3, 7, 0, 0, 4, 8, 0, 44, 5, 3, 1, 4, 46, 5, 0, 3, 5, 0, 45, 3, 4, 2, 48, 5, 0, 5, 3, 5, 0, 5, 2, 0, 26, 6, 0, 5, 40, 6, 130, 39, 145, 0, 5, 3, 0, 26, 6, 0, 5, 40, 6, 143, 39, 227, 39, 247, 54, 191, 4, 3, 5, 0, 42, 6, 5, 1, 0, 0, 5, 2, 0, 26, 6, 0, 5, 40, 6, 170, 55, 39, 265, 0, 5, 9, 0, 8, 6, 3, 5, 0, 5, 3, 0, 43, 7, 3, 6, 1, 5, 55, 39, 209, 0, 5, 9, 0, 8, 6, 3, 5, 0, 5, 6, 0, 43, 7, 3, 6, 1, 5, 0, 5, 9, 0, 8, 6, 3, 5, 0, 5, 10, 0, 43, 7, 3, 6, 1, 5, 0, 5, 9, 0, 8, 6, 3, 5, 0, 5, 11, 0, 43, 7, 3, 6, 1, 5, 39, 265, 0, 5, 9, 0, 8, 6, 3, 5, 0, 5, 12, 0, 43, 7, 3, 6, 1, 5, 0, 5, 9, 0, 8, 6, 3, 5, 0, 5, 13, 0, 43, 7, 3, 6, 1, 5, 45, 3, 0, 5, 5, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break/source.js b/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break/source.js new file mode 100644 index 00000000..88a71901 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-statements/try-ends/switch-break/source.js @@ -0,0 +1,3 @@ +function g(i) { if (i === 3) throw new Error("x"); } +function f(x) { var out = []; switch (x) { case 1: try { g(x); if (x === 1) break; out.push(2); } catch (e) { out.push(3); } out.push(4); case 2: out.push(5); break; default: out.push(6); } out.push(7); return out; } +window.TEST_OUTPUT = [f(1), f(2), f(9)]; diff --git a/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms-cff/encoded.js b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms-cff/encoded.js new file mode 100644 index 00000000..14ce6da1 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms-cff/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */41095, /* 1 */0, /* 2 */65177, /* 3 */13465, /* 4 */14440, /* 5 */1857, /* 6 */4804, /* 7 */15832, /* 8 */14319, /* 9 */14007, /* 10 */15622, /* 11 */61494, /* 12 */37215, /* 13 */17035, /* 14 */33087, /* 15 */22065, /* 16 */39335, /* 17 */15234, /* 18 */6153, /* 19 */34612, /* 20 */6688, /* 21 */8684, /* 22 */22265, /* 23 */216, /* 24 */1853, /* 25 */39833, /* 26 */13950, /* 27 */5597, /* 28 */31407, /* 29 */13758, /* 30 */40600, /* 31 */31058, /* 32 */1472, /* 33 */8543, /* 34 */31013, /* 35 */13615, /* 36 */29657, /* 37 */35609, /* 38 */3755, /* 39 */21909, /* 40 */1, /* 41 */"value", /* 42 */"length", /* 43 */2, /* 44 */"push", /* 45 */undefined, /* 46 */"thrown", /* 47 */"message", /* 48 */"window", /* 49 */"TEST_OUTPUT", /* 50 */57001, /* 51 */9773, /* 52 */51892, /* 53 */223, /* 54 */4886, /* 55 */36541, /* 56 */36016, /* 57 */50168, /* 58 */8162, /* 59 */4358, /* 60 */"Error", /* 61 */"f", /* 62 */"g:", /* 63 */34463, /* 64 */35129, /* 65 */16073, /* 66 */6526, /* 67 */5527, /* 68 */6337, /* 69 */10519, /* 70 */3582, /* 71 */13362, /* 72 */24724, /* 73 */1354, /* 74 */24889, /* 75 */56414, /* 76 */962, /* 77 */50010, /* 78 */33736, /* 79 */11761, /* 80 */19414, /* 81 */14921, /* 82 */25626, /* 83 */1362, /* 84 */26347, /* 85 */44180, /* 86 */35464, /* 87 */8879, /* 88 */7981, /* 89 */10070, /* 90 */14601, /* 91 */51376, /* 92 */19082, /* 93 */1786, /* 94 */16453, /* 95 */41069, /* 96 */3, /* 97 */4, /* 98 */38752, /* 99 */818, /* 100 */62546, /* 101 */44659, /* 102 */7835, /* 103 */213, /* 104 */31230, /* 105 */10054, /* 106 */32551, /* 107 */25240, /* 108 */6, /* 109 */5, /* 110 */41127, /* 111 */51141, /* 112 */15368, /* 113 */35299, /* 114 */42555, /* 115 */40753, /* 116 */48392, /* 117 */51067, /* 118 */10413, /* 119 */"re", /* 120 */7, /* 121 */65319, /* 122 */3150, /* 123 */41857, /* 124 */23462, /* 125 */26358, /* 126 */26009, /* 127 */27674, /* 128 */16541, /* 129 */15730, /* 130 */9649, /* 131 */29338, /* 132 */371, /* 133 */39703, /* 134 */19282, /* 135 */35337, /* 136 */24655, /* 137 */310, /* 138 */22921, /* 139 */18774, /* 140 */38170, /* 141 */29185, /* 142 */18311, /* 143 */29639, /* 144 */62062, /* 145 */37701, /* 146 */16931, /* 147 */25793, /* 148 */3005, /* 149 */9, /* 150 */"inner", /* 151 */22942, /* 152 */27179, /* 153 */23644, /* 154 */13385, /* 155 */17024, /* 156 */1590, /* 157 */33551, /* 158 */7673, /* 159 */18677, /* 160 */10154, /* 161 */22456, /* 162 */13113, /* 163 */39136, /* 164 */"outer", /* 165 */11, /* 166 */42953, /* 167 */42139, /* 168 */21048, /* 169 */40266, /* 170 */8178, /* 171 */6488, /* 172 */12804, /* 173 */14325, /* 174 */16492, /* 175 */"fin", /* 176 */54971, /* 177 */47496, /* 178 */13022, /* 179 */9902, /* 180 */11810, /* 181 */23046, /* 182 */37612, /* 183 */41627, /* 184 */8290, /* 185 */47320, /* 186 */52010, /* 187 */13979, /* 188 */5915, /* 189 */61937, /* 190 */28932, /* 191 */7561, /* 192 */25681, /* 193 */11749, /* 194 */13902, /* 195 */12879, /* 196 */3049, /* 197 */20261, /* 198 */6198, /* 199 */3536, /* 200 */3119, /* 201 */10, /* 202 */27542, /* 203 */62844, /* 204 */12486, /* 205 */54433, /* 206 */9782, /* 207 */41941, /* 208 */23495, /* 209 */17584, /* 210 */17457, /* 211 */16307, /* 212 */41, /* 213 */45154, /* 214 */63022, /* 215 */22563, /* 216 */26773, /* 217 */2896, /* 218 */12050, /* 219 */10764, /* 220 */23735, /* 221 */427, /* 222 */30302, /* 223 */42, /* 224 */63101, /* 225 */34178, /* 226 */40928, /* 227 */4429, /* 228 */25096, /* 229 */37524, /* 230 */23758, /* 231 */22822, /* 232 */60754, /* 233 */51308, /* 234 */2722, /* 235 */5729, /* 236 */3275, /* 237 */11337, /* 238 */25351, /* 239 */16461, /* 240 */14836, /* 241 */29428, /* 242 */52346, /* 243 */58534, /* 244 */207, /* 245 */6691, /* 246 */13145, /* 247 */26912, /* 248 */34285, /* 249 */"name", /* 250 */"ReferenceError", /* 251 */"definitely_missing_global", /* 252 */null, /* 253 */22849, /* 254 */48306, /* 255 */48820, /* 256 */24372, /* 257 */22628, /* 258 */5509, /* 259 */22179, /* 260 */29377, /* 261 */16103, /* 262 */25830, /* 263 */15665, /* 264 */9818, /* 265 */1597, /* 266 */33656, /* 267 */10051, /* 268 */22872, /* 269 */4443, /* 270 */8430, /* 271 */13075, /* 272 */565, /* 273 */26821, /* 274 */3445, /* 275 */26672, /* 276 */39003, /* 277 */189, /* 278 */45067, /* 279 */30775, /* 280 */12296, /* 281 */1385, /* 282 */17239, /* 283 */24524, /* 284 */23525, /* 285 */18219, /* 286 */12232, /* 287 */30484, /* 288 */25095, /* 289 */31004, /* 290 */5801, /* 291 */6327, /* 292 */44824, /* 293 */10286, /* 294 */36466, /* 295 */9926, /* 296 */3380, /* 297 */5732, /* 298 */22701, /* 299 */30238, /* 300 */28414, /* 301 */2088, /* 302 */100, /* 303 */8122, /* 304 */31354, /* 305 */23449, /* 306 */15327, /* 307 */27384, /* 308 */28105, /* 309 */47272, /* 310 */21748, /* 311 */10578, /* 312 */3186, /* 313 */8172, /* 314 */26245, /* 315 */47432, /* 316 */39328, /* 317 */4194, /* 318 */36087, /* 319 */32517, /* 320 */26053, /* 321 */22398, /* 322 */20476, /* 323 */60863, /* 324 */14044, /* 325 */27779, /* 326 */7226, /* 327 */31703, /* 328 */57561, /* 329 */24189, /* 330 */15340, /* 331 */8578, /* 332 */27153, /* 333 */389, /* 334 */7055, /* 335 */52979, /* 336 */42494, /* 337 */29098, /* 338 */28002, /* 339 */17017, /* 340 */28526, /* 341 */39882, /* 342 */26230]; +var BYTECODE = [4, 55, 0, 56, 0, 0, 5, 57, 56, 0, 58, 1, 0, 5, 59, 58, 0, 60, 2, 0, 27, 61, 57, 60, 40, 61, 765, 0, 62, 3, 0, 5, 59, 62, 26, 63, 57, 59, 40, 63, 43, 39, 1227, 0, 64, 4, 0, 11, 65, 59, 64, 5, 59, 65, 26, 66, 57, 59, 40, 66, 63, 39, 1457, 0, 67, 5, 0, 11, 68, 59, 67, 5, 59, 68, 26, 69, 57, 59, 40, 69, 83, 39, 995, 0, 70, 6, 0, 11, 71, 59, 70, 5, 59, 71, 26, 72, 57, 59, 40, 72, 103, 39, 893, 0, 73, 7, 0, 12, 74, 59, 73, 5, 59, 74, 26, 75, 57, 59, 40, 75, 123, 39, 1572, 0, 76, 8, 0, 12, 77, 59, 76, 5, 59, 77, 26, 78, 57, 59, 40, 78, 143, 39, 1544, 0, 79, 9, 0, 11, 80, 59, 79, 5, 59, 80, 26, 81, 57, 59, 40, 81, 163, 39, 1065, 0, 82, 10, 0, 12, 83, 59, 82, 5, 59, 83, 26, 84, 57, 59, 40, 84, 183, 39, 1503, 0, 85, 11, 0, 11, 86, 59, 85, 5, 59, 86, 26, 87, 57, 59, 40, 87, 203, 39, 1350, 0, 88, 12, 0, 12, 89, 59, 88, 5, 59, 89, 26, 90, 57, 59, 40, 90, 223, 39, 1374, 0, 91, 13, 0, 12, 92, 59, 91, 5, 59, 92, 26, 93, 57, 59, 40, 93, 243, 39, 1252, 0, 94, 14, 0, 11, 95, 59, 94, 5, 59, 95, 26, 96, 57, 59, 40, 96, 263, 39, 1438, 0, 97, 15, 0, 11, 98, 59, 97, 5, 59, 98, 26, 99, 57, 59, 40, 99, 283, 39, 1280, 0, 100, 16, 0, 12, 101, 59, 100, 5, 59, 101, 26, 102, 57, 59, 40, 102, 303, 39, 765, 0, 103, 17, 0, 11, 104, 59, 103, 5, 59, 104, 26, 105, 57, 59, 40, 105, 323, 39, 832, 0, 106, 18, 0, 11, 107, 59, 106, 5, 59, 107, 26, 108, 57, 59, 40, 108, 343, 39, 1203, 0, 109, 19, 0, 12, 110, 59, 109, 5, 59, 110, 26, 111, 57, 59, 40, 111, 363, 39, 815, 0, 112, 20, 0, 11, 113, 59, 112, 5, 59, 113, 26, 114, 57, 59, 40, 114, 383, 39, 1523, 0, 115, 21, 0, 11, 116, 59, 115, 5, 59, 116, 26, 117, 57, 59, 40, 117, 403, 39, 1483, 0, 118, 22, 0, 11, 119, 59, 118, 5, 59, 119, 26, 120, 57, 59, 40, 120, 423, 39, 942, 0, 121, 23, 0, 11, 122, 59, 121, 5, 59, 122, 26, 123, 57, 59, 40, 123, 443, 39, 1338, 0, 124, 24, 0, 11, 125, 59, 124, 5, 59, 125, 26, 126, 57, 59, 40, 126, 463, 39, 913, 0, 127, 25, 0, 12, 128, 59, 127, 5, 59, 128, 26, 129, 57, 59, 40, 129, 483, 39, 855, 0, 130, 26, 0, 11, 131, 59, 130, 5, 59, 131, 26, 132, 57, 59, 40, 132, 503, 39, 1383, 0, 133, 27, 0, 11, 134, 59, 133, 5, 59, 134, 26, 135, 57, 59, 40, 135, 523, 39, 1117, 0, 136, 28, 0, 11, 137, 59, 136, 5, 59, 137, 26, 138, 57, 59, 40, 138, 543, 39, 1417, 0, 139, 29, 0, 12, 140, 59, 139, 5, 59, 140, 26, 141, 57, 59, 40, 141, 563, 39, 1141, 0, 142, 30, 0, 12, 143, 59, 142, 5, 59, 143, 26, 144, 57, 59, 40, 144, 583, 39, 1312, 0, 145, 31, 0, 11, 146, 59, 145, 5, 59, 146, 26, 147, 57, 59, 40, 147, 603, 39, 1246, 0, 148, 32, 0, 12, 149, 59, 148, 5, 59, 149, 26, 150, 57, 59, 40, 150, 623, 39, 1191, 0, 151, 33, 0, 11, 152, 59, 151, 5, 59, 152, 26, 153, 57, 59, 40, 153, 643, 39, 1159, 0, 154, 34, 0, 12, 155, 59, 154, 5, 59, 155, 26, 156, 57, 59, 40, 156, 663, 39, 789, 0, 157, 35, 0, 11, 158, 59, 157, 5, 59, 158, 26, 159, 57, 59, 40, 159, 683, 39, 1590, 0, 160, 36, 0, 12, 161, 59, 160, 5, 59, 161, 26, 162, 57, 59, 40, 162, 703, 39, 1044, 0, 163, 37, 0, 11, 164, 59, 163, 5, 59, 164, 26, 165, 57, 59, 40, 165, 723, 39, 972, 0, 166, 38, 0, 12, 167, 59, 166, 5, 59, 167, 26, 168, 57, 59, 40, 168, 743, 39, 1010, 0, 169, 39, 0, 12, 170, 59, 169, 5, 59, 170, 26, 171, 57, 59, 40, 171, 763, 39, 1085, 39, 16, 5, 21, 36, 47, 173, 8476, 1, 78, 1, 0, 1, 3, 5, 22, 173, 1, 172, 24412, 11, 57, 57, 172, 39, 16, 5, 14, 34, 47, 173, 5612, 1, 50, 2, 0, 1, 3, 1, 4, 5, 15, 173, 1, 172, 6097, 12, 57, 57, 172, 39, 16, 48, 173, 2, 49, 50, 5, 29, 173, 1, 172, 50827, 11, 57, 57, 172, 39, 16, 4, 1, 47, 173, 1616, 1, 39, 1, 0, 1, 2, 5, 3, 173, 1, 172, 6139, 11, 57, 57, 172, 39, 16, 0, 173, 40, 0, 5, 16, 173, 48, 38, 19, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 20, 17, 18, 19, 21, 22, 23, 24, 1, 172, 9709, 12, 57, 57, 172, 39, 16, 0, 173, 40, 0, 20, 174, 26, 173, 5, 27, 174, 1, 172, 15242, 12, 57, 57, 172, 39, 16, 13, 173, 42, 43, 22, 174, 26, 173, 41, 174, 933, 1, 172, 12179, 12, 57, 57, 172, 39, 16, 1, 172, 17776, 12, 57, 57, 172, 39, 16, 47, 173, 9258, 1, 72, 1, 0, 1, 3, 5, 23, 173, 47, 37, 9942, 1, 72, 1, 0, 1, 3, 1, 172, 31539, 12, 57, 57, 172, 39, 16, 0, 47, 41, 0, 8, 173, 25, 27, 43, 48, 25, 173, 1, 28, 1, 172, 13917, 11, 57, 57, 172, 39, 16, 43, 173, 51, 52, 1, 53, 1, 172, 11340, 12, 57, 57, 172, 39, 16, 47, 173, 3587, 1, 52, 2, 0, 1, 3, 1, 4, 5, 10, 173, 47, 33, 4050, 1, 52, 2, 0, 1, 3, 1, 4, 1, 172, 22928, 12, 57, 57, 172, 39, 16, 0, 173, 42, 0, 8, 42, 25, 173, 0, 43, 43, 0, 1, 172, 52163, 11, 57, 57, 172, 39, 16, 5, 173, 26, 0, 173, 40, 0, 11, 54, 26, 173, 1, 172, 32067, 11, 57, 57, 172, 39, 16, 47, 173, 6035, 1, 41, 2, 0, 1, 16, 1, 4, 5, 17, 173, 47, 35, 6367, 1, 38, 1, 0, 1, 4, 1, 172, 21932, 11, 57, 57, 172, 39, 16, 5, 18, 35, 47, 173, 6709, 1, 40, 1, 0, 1, 4, 5, 19, 173, 1, 172, 27641, 12, 57, 57, 172, 39, 16, 48, 173, 2, 47, 48, 5, 29, 173, 55, 1, 172, 13758, 11, 57, 57, 172, 39, 16, 47, 173, 1908, 1, 7, 1, 0, 1, 2, 5, 4, 173, 47, 31, 1941, 1, 42, 2, 0, 1, 3, 1, 4, 1, 172, 19329, 12, 57, 57, 172, 39, 16, 5, 26, 41, 1, 172, 38512, 12, 57, 57, 172, 39, 16, 0, 173, 44, 0, 8, 52, 51, 173, 48, 53, 4, 27, 28, 29, 2, 1, 172, 17486, 12, 57, 57, 172, 39, 16, 26, 173, 44, 45, 5, 28, 173, 48, 46, 0, 1, 172, 29666, 11, 57, 57, 172, 39, 16, 0, 173, 45, 0, 45, 173, 1, 172, 21608, 22, 172, 57, 172, 41, 172, 1271, 1, 172, 17035, 11, 57, 57, 172, 39, 16, 1, 172, 7244, 12, 57, 57, 172, 39, 16, 47, 173, 2611, 1, 45, 2, 0, 1, 3, 1, 4, 5, 7, 173, 47, 32, 3021, 1, 37, 1, 0, 1, 3, 1, 172, 35360, 12, 57, 57, 172, 39, 16, 5, 11, 33, 47, 173, 4533, 1, 47, 2, 0, 1, 3, 1, 4, 5, 12, 173, 1, 172, 17354, 11, 57, 57, 172, 39, 16, 5, 26, 54, 1, 172, 50310, 12, 57, 57, 172, 39, 16, 1, 57, 64294, 0, 49, 46, 0, 0, 173, 47, 0, 8, 50, 30, 173, 1, 172, 51658, 12, 57, 57, 172, 39, 16, 1, 172, 2683, 11, 57, 57, 172, 39, 16, 47, 173, 4942, 1, 40, 2, 0, 1, 3, 1, 4, 5, 13, 173, 47, 34, 5243, 1, 46, 2, 0, 1, 3, 1, 4, 1, 172, 10238, 12, 57, 57, 172, 39, 16, 2, 173, 48, 0, 0, 174, 49, 0, 8, 51, 173, 174, 1, 172, 16215, 12, 57, 57, 172, 39, 16, 5, 2, 46, 0, 29, 45, 0, 54, 1350, 30, 1, 172, 7343, 12, 57, 57, 172, 39, 16, 5, 5, 31, 47, 173, 2272, 1, 40, 2, 0, 1, 3, 1, 4, 5, 6, 173, 1, 172, 37291, 11, 57, 57, 172, 39, 16, 48, 173, 0, 9, 39, 40, 173, 0, 41, 1, 0, 1, 172, 10683, 11, 57, 57, 172, 39, 16, 5, 25, 38, 2, 39, 48, 0, 0, 40, 49, 0, 1, 172, 25208, 11, 57, 57, 172, 39, 16, 0, 173, 40, 0, 16, 44, 26, 173, 0, 45, 40, 0, 1, 172, 5859, 12, 57, 57, 172, 39, 16, 47, 173, 7024, 1, 55, 0, 0, 5, 20, 173, 47, 36, 7529, 1, 94, 1, 0, 1, 3, 1, 172, 21446, 11, 57, 57, 172, 39, 16, 5, 24, 37, 48, 173, 0, 5, 2, 173, 1, 172, 6225, 12, 57, 57, 172, 39, 16, 5, 8, 32, 47, 173, 3343, 1, 36, 2, 0, 1, 3, 1, 4, 5, 9, 173, 1, 172, 2197, 11, 57, 57, 172, 39, 16, 4, 5, 0, 6, 50, 0, 5, 7, 6, 0, 8, 1, 0, 5, 9, 8, 0, 10, 51, 0, 27, 11, 7, 10, 40, 11, 1801, 0, 12, 52, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 1659, 39, 1822, 0, 14, 53, 0, 11, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 1679, 39, 1801, 0, 17, 54, 0, 11, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 1699, 39, 1872, 0, 20, 55, 0, 12, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 1719, 39, 1890, 0, 23, 56, 0, 11, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 1739, 39, 1899, 0, 26, 57, 0, 12, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 1759, 39, 1837, 0, 29, 58, 0, 11, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 1779, 39, 1816, 0, 32, 59, 0, 11, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 1799, 39, 1828, 39, 1632, 2, 36, 60, 0, 0, 37, 61, 0, 44, 38, 36, 1, 37, 46, 38, 0, 36, 45, 0, 45, 36, 0, 36, 40, 0, 45, 36, 1, 35, 4358, 12, 7, 7, 35, 39, 1632, 8, 36, 3, 4, 0, 37, 61, 0, 43, 38, 3, 36, 1, 37, 41, 0, 1863, 1, 35, 45584, 11, 7, 7, 35, 39, 1632, 1, 35, 45807, 11, 7, 7, 35, 39, 1632, 4, 2, 3, 3, 0, 0, 4, 44, 0, 1, 35, 50693, 12, 7, 7, 35, 39, 1632, 1, 35, 0, 11, 7, 7, 35, 39, 1632, 1, 35, 0, 11, 7, 7, 35, 39, 1632, 4, 2, 3, 3, 0, 0, 4, 44, 0, 8, 5, 3, 4, 0, 4, 62, 0, 11, 6, 4, 0, 43, 4, 3, 5, 1, 6, 0, 3, 45, 0, 45, 3, 4, 5, 0, 6, 63, 0, 5, 7, 6, 0, 8, 1, 0, 5, 9, 8, 0, 10, 64, 0, 27, 11, 7, 10, 40, 11, 2146, 0, 12, 65, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 1984, 39, 2222, 0, 14, 66, 0, 11, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 2004, 39, 2146, 0, 17, 67, 0, 11, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 2024, 39, 2254, 0, 20, 68, 0, 11, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 2044, 39, 2237, 0, 23, 69, 0, 11, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 2064, 39, 2161, 0, 26, 70, 0, 12, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 2084, 39, 2228, 0, 29, 71, 0, 12, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 2104, 39, 2182, 0, 32, 72, 0, 12, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 2124, 39, 2263, 0, 35, 73, 0, 12, 36, 9, 35, 5, 9, 36, 26, 37, 7, 9, 40, 37, 2144, 39, 2194, 39, 1957, 42, 39, 4, 1, 0, 55, 1, 38, 22383, 11, 7, 7, 38, 39, 1957, 3, 39, 1, 0, 40, 40, 0, 42, 41, 39, 1, 40, 1, 38, 28909, 12, 7, 7, 38, 39, 1957, 1, 7, 28038, 1, 38, 16944, 11, 7, 7, 38, 39, 1957, 1, 38, 19244, 22, 38, 7, 38, 41, 38, 2213, 1, 38, 14113, 11, 7, 7, 38, 39, 1957, 1, 38, 1354, 11, 7, 7, 38, 39, 1957, 0, 39, 45, 0, 45, 39, 1, 38, 18801, 12, 7, 7, 38, 39, 1957, 4, 2, 54, 2182, 3, 3, 4, 0, 1, 38, 11864, 12, 7, 7, 38, 39, 1957, 1, 38, 6337, 11, 7, 7, 38, 39, 1957, 1, 38, 12759, 11, 7, 7, 38, 39, 1957, 4, 7, 0, 8, 74, 0, 5, 9, 8, 0, 10, 1, 0, 5, 11, 10, 0, 12, 75, 0, 27, 13, 9, 12, 40, 13, 2457, 0, 14, 76, 0, 5, 11, 14, 26, 15, 9, 11, 40, 15, 2315, 39, 2515, 0, 16, 77, 0, 11, 17, 11, 16, 5, 11, 17, 26, 18, 9, 11, 40, 18, 2335, 39, 2537, 0, 19, 78, 0, 12, 20, 11, 19, 5, 11, 20, 26, 21, 9, 11, 40, 21, 2355, 39, 2560, 0, 22, 79, 0, 12, 23, 11, 22, 5, 11, 23, 26, 24, 9, 11, 40, 24, 2375, 39, 2467, 0, 25, 80, 0, 11, 26, 11, 25, 5, 11, 26, 26, 27, 9, 11, 40, 27, 2395, 39, 2574, 0, 28, 81, 0, 11, 29, 11, 28, 5, 11, 29, 26, 30, 9, 11, 40, 30, 2415, 39, 2487, 0, 31, 82, 0, 11, 32, 11, 31, 5, 11, 32, 26, 33, 9, 11, 40, 33, 2435, 39, 2457, 0, 34, 83, 0, 12, 35, 11, 34, 5, 11, 35, 26, 36, 9, 11, 40, 36, 2455, 39, 2592, 39, 2288, 55, 1, 37, 48200, 12, 9, 9, 37, 39, 2288, 54, 2537, 4, 3, 38, 0, 42, 39, 38, 1, 0, 1, 37, 59961, 11, 9, 9, 37, 39, 2288, 1, 37, 1259, 22, 37, 9, 37, 41, 37, 2506, 1, 37, 25626, 11, 9, 9, 37, 39, 2288, 1, 37, 25626, 11, 9, 9, 37, 39, 2288, 0, 38, 47, 0, 8, 39, 4, 38, 42, 38, 6, 1, 39, 1, 37, 16274, 11, 9, 9, 37, 39, 2288, 1, 9, 50972, 0, 38, 40, 0, 5, 3, 38, 0, 5, 40, 0, 1, 37, 13102, 11, 9, 9, 37, 39, 2288, 3, 38, 1, 42, 39, 38, 1, 3, 0, 38, 45, 0, 45, 38, 4, 2, 0, 38, 1, 0, 5, 3, 38, 1, 37, 19414, 12, 9, 9, 37, 39, 2288, 11, 38, 3, 5, 5, 3, 38, 3, 6, 1, 1, 37, 63112, 12, 9, 9, 37, 39, 2288, 4, 5, 0, 6, 84, 0, 5, 7, 6, 0, 8, 1, 0, 5, 9, 8, 0, 10, 85, 0, 27, 11, 7, 10, 40, 11, 2836, 0, 12, 86, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 2654, 39, 2931, 0, 14, 87, 0, 11, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 2674, 39, 2836, 0, 17, 88, 0, 11, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 2694, 39, 2940, 0, 20, 89, 0, 12, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 2714, 39, 2869, 0, 23, 90, 0, 11, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 2734, 39, 2955, 0, 26, 91, 0, 12, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 2754, 39, 2875, 0, 29, 92, 0, 11, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 2774, 39, 2903, 0, 32, 93, 0, 11, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 2794, 39, 3004, 0, 35, 94, 0, 11, 36, 9, 35, 5, 9, 36, 26, 37, 7, 9, 40, 37, 2814, 39, 2983, 0, 38, 95, 0, 12, 39, 9, 38, 5, 9, 39, 26, 40, 7, 9, 40, 40, 2834, 39, 2860, 39, 2627, 1, 7, 44343, 3, 42, 1, 0, 43, 96, 0, 42, 44, 42, 1, 43, 1, 41, 1543, 12, 7, 7, 41, 39, 2627, 1, 41, 41069, 11, 7, 7, 41, 39, 2627, 0, 42, 45, 0, 45, 42, 1, 41, 22054, 22, 41, 7, 41, 41, 41, 2894, 1, 41, 20868, 11, 7, 7, 41, 39, 2627, 1, 41, 37321, 11, 7, 7, 41, 39, 2627, 1, 41, 30689, 22, 41, 7, 41, 41, 41, 2922, 1, 41, 10903, 11, 7, 7, 41, 39, 2627, 1, 41, 0, 11, 7, 7, 41, 39, 2627, 1, 41, 33733, 12, 7, 7, 41, 39, 2627, 42, 42, 4, 1, 0, 55, 1, 41, 9524, 12, 7, 7, 41, 39, 2627, 1, 41, 42352, 22, 41, 7, 41, 41, 41, 2974, 1, 41, 12512, 12, 7, 7, 41, 39, 2627, 1, 41, 21391, 12, 7, 7, 41, 39, 2627, 3, 42, 1, 0, 43, 97, 0, 42, 44, 42, 1, 43, 1, 41, 546, 12, 7, 7, 41, 39, 2627, 4, 2, 54, 2836, 3, 3, 4, 0, 1, 41, 25977, 11, 7, 7, 41, 39, 2627, 4, 5, 0, 6, 98, 0, 5, 7, 6, 0, 8, 1, 0, 5, 9, 8, 0, 10, 99, 0, 27, 11, 7, 10, 40, 11, 3206, 0, 12, 100, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 3064, 39, 3337, 0, 14, 101, 0, 12, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 3084, 39, 3206, 0, 17, 102, 0, 12, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 3104, 39, 3322, 0, 20, 103, 0, 11, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 3124, 39, 3328, 0, 23, 104, 0, 11, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 3144, 39, 3238, 0, 26, 105, 0, 12, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 3164, 39, 3294, 0, 29, 106, 0, 11, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 3184, 39, 3266, 0, 32, 107, 0, 12, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 3204, 39, 3221, 39, 3037, 42, 36, 4, 1, 0, 55, 1, 35, 7835, 12, 7, 7, 35, 39, 3037, 4, 2, 54, 3328, 3, 3, 4, 0, 1, 35, 20865, 12, 7, 7, 35, 39, 3037, 1, 35, 46887, 22, 35, 7, 35, 41, 35, 3257, 1, 35, 2743, 12, 7, 7, 35, 39, 3037, 1, 35, 2743, 12, 7, 7, 35, 39, 3037, 1, 35, 25212, 22, 35, 7, 35, 41, 35, 3285, 1, 35, 1446, 12, 7, 7, 35, 39, 3037, 1, 35, 32551, 12, 7, 7, 35, 39, 3037, 1, 35, 28169, 22, 35, 7, 35, 41, 35, 3313, 1, 35, 13554, 12, 7, 7, 35, 39, 3037, 1, 35, 21176, 12, 7, 7, 35, 39, 3037, 0, 36, 108, 0, 45, 36, 1, 7, 10265, 0, 36, 109, 0, 45, 36, 0, 36, 45, 0, 45, 36, 4, 5, 0, 6, 110, 0, 5, 7, 6, 0, 8, 1, 0, 5, 9, 8, 0, 10, 111, 0, 27, 11, 7, 10, 40, 11, 3508, 0, 12, 112, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 3386, 39, 3531, 0, 14, 113, 0, 11, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 3406, 39, 3540, 0, 17, 114, 0, 12, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 3426, 39, 3566, 0, 20, 115, 0, 11, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 3446, 39, 3557, 0, 23, 116, 0, 12, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 3466, 39, 3508, 0, 26, 117, 0, 11, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 3486, 39, 3572, 0, 29, 118, 0, 12, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 3506, 39, 3514, 39, 3359, 0, 33, 45, 0, 45, 33, 4, 2, 54, 3540, 3, 3, 4, 0, 1, 32, 10413, 11, 7, 7, 32, 39, 3359, 1, 32, 36172, 11, 7, 7, 32, 39, 3359, 1, 7, 50667, 3, 33, 1, 0, 34, 119, 0, 42, 35, 33, 1, 34, 46, 3, 1, 32, 2675, 11, 7, 7, 32, 39, 3359, 0, 33, 120, 0, 45, 33, 42, 33, 4, 1, 0, 55, 1, 32, 43428, 12, 7, 7, 32, 39, 3359, 4, 7, 0, 8, 121, 0, 5, 9, 8, 0, 10, 1, 0, 5, 11, 10, 0, 12, 122, 0, 27, 13, 9, 12, 40, 13, 3852, 0, 14, 123, 0, 5, 11, 14, 26, 15, 9, 11, 40, 15, 3630, 39, 3875, 0, 16, 124, 0, 11, 17, 11, 16, 5, 11, 17, 26, 18, 9, 11, 40, 18, 3650, 39, 4032, 0, 19, 125, 0, 12, 20, 11, 19, 5, 11, 20, 26, 21, 9, 11, 40, 21, 3670, 39, 3861, 0, 22, 126, 0, 12, 23, 11, 22, 5, 11, 23, 26, 24, 9, 11, 40, 24, 3690, 39, 3900, 0, 25, 127, 0, 11, 26, 11, 25, 5, 11, 26, 26, 27, 9, 11, 40, 27, 3710, 39, 3891, 0, 28, 128, 0, 12, 29, 11, 28, 5, 11, 29, 26, 30, 9, 11, 40, 30, 3730, 39, 3916, 0, 31, 129, 0, 11, 32, 11, 31, 5, 11, 32, 26, 33, 9, 11, 40, 33, 3750, 39, 3926, 0, 34, 130, 0, 12, 35, 11, 34, 5, 11, 35, 26, 36, 9, 11, 40, 36, 3770, 39, 3978, 0, 37, 131, 0, 11, 38, 11, 37, 5, 11, 38, 26, 39, 9, 11, 40, 39, 3790, 39, 3987, 0, 40, 132, 0, 11, 41, 11, 40, 5, 11, 41, 26, 42, 9, 11, 40, 42, 3810, 39, 4007, 0, 43, 133, 0, 12, 44, 11, 43, 5, 11, 44, 26, 45, 9, 11, 40, 45, 3830, 39, 3950, 0, 46, 134, 0, 12, 47, 11, 46, 5, 11, 47, 26, 48, 9, 11, 40, 48, 3850, 39, 3852, 39, 3603, 1, 49, 39736, 11, 9, 9, 49, 39, 3603, 3, 50, 1, 42, 51, 50, 1, 3, 0, 50, 45, 0, 45, 50, 0, 50, 43, 0, 5, 3, 50, 1, 49, 1231, 12, 9, 9, 49, 39, 3603, 1, 49, 1665, 12, 9, 9, 49, 39, 3603, 0, 50, 40, 0, 5, 3, 50, 1, 49, 27674, 11, 9, 9, 49, 39, 3603, 55, 1, 49, 14876, 11, 9, 9, 49, 39, 3603, 1, 9, 39815, 0, 50, 47, 0, 8, 5, 4, 50, 0, 6, 61, 0, 1, 49, 20060, 11, 9, 9, 49, 39, 3603, 1, 49, 38394, 22, 49, 9, 49, 41, 49, 3969, 1, 49, 19282, 12, 9, 9, 49, 39, 3603, 1, 49, 7220, 12, 9, 9, 49, 39, 3603, 1, 49, 29338, 11, 9, 9, 49, 39, 3603, 54, 3926, 4, 3, 50, 0, 42, 51, 50, 1, 0, 1, 49, 35419, 12, 9, 9, 49, 39, 3603, 26, 50, 5, 6, 41, 50, 4023, 1, 49, 18018, 12, 9, 9, 49, 39, 3603, 1, 49, 46923, 12, 9, 9, 49, 39, 3603, 4, 2, 0, 50, 1, 0, 5, 3, 50, 1, 49, 5815, 12, 9, 9, 49, 39, 3603, 4, 6, 0, 7, 135, 0, 5, 8, 7, 0, 9, 1, 0, 5, 10, 9, 0, 11, 136, 0, 27, 12, 8, 11, 40, 12, 4315, 0, 13, 137, 0, 5, 10, 13, 26, 14, 8, 10, 40, 14, 4093, 39, 4435, 0, 15, 138, 0, 11, 16, 10, 15, 5, 10, 16, 26, 17, 8, 10, 40, 17, 4113, 39, 4315, 0, 18, 139, 0, 12, 19, 10, 18, 5, 10, 19, 26, 20, 8, 10, 40, 20, 4133, 39, 4402, 0, 21, 140, 0, 11, 22, 10, 21, 5, 10, 22, 26, 23, 8, 10, 40, 23, 4153, 39, 4444, 0, 24, 141, 0, 12, 25, 10, 24, 5, 10, 25, 26, 26, 8, 10, 40, 26, 4173, 39, 4330, 0, 27, 142, 0, 11, 28, 10, 27, 5, 10, 28, 26, 29, 8, 10, 40, 29, 4193, 39, 4411, 0, 30, 143, 0, 12, 31, 10, 30, 5, 10, 31, 26, 32, 8, 10, 40, 32, 4213, 39, 4374, 0, 33, 144, 0, 11, 34, 10, 33, 5, 10, 34, 26, 35, 8, 10, 40, 35, 4233, 39, 4472, 0, 36, 145, 0, 12, 37, 10, 36, 5, 10, 37, 26, 38, 8, 10, 40, 38, 4253, 39, 4478, 0, 39, 146, 0, 12, 40, 10, 39, 5, 10, 40, 26, 41, 8, 10, 40, 41, 4273, 39, 4353, 0, 42, 147, 0, 11, 43, 10, 42, 5, 10, 43, 26, 44, 8, 10, 40, 44, 4293, 39, 4516, 0, 45, 148, 0, 11, 46, 10, 45, 5, 10, 46, 26, 47, 8, 10, 40, 47, 4313, 39, 4506, 39, 4066, 42, 49, 5, 1, 0, 55, 1, 48, 13687, 12, 8, 8, 48, 39, 4066, 1, 8, 13442, 54, 4411, 4, 3, 49, 0, 42, 50, 49, 1, 0, 1, 48, 24900, 11, 8, 8, 48, 39, 4066, 3, 49, 1, 0, 50, 149, 0, 42, 51, 49, 1, 50, 1, 48, 54632, 11, 8, 8, 48, 39, 4066, 1, 48, 31735, 22, 48, 8, 48, 41, 48, 4393, 1, 48, 1804, 12, 8, 8, 48, 39, 4066, 1, 48, 29639, 11, 8, 8, 48, 39, 4066, 1, 48, 38170, 11, 8, 8, 48, 39, 4066, 1, 8, 31753, 3, 49, 1, 0, 50, 150, 0, 42, 51, 49, 1, 50, 1, 48, 31443, 12, 8, 8, 48, 39, 4066, 1, 48, 9234, 11, 8, 8, 48, 39, 4066, 1, 48, 53622, 22, 48, 8, 48, 41, 48, 4463, 1, 48, 29185, 12, 8, 8, 48, 39, 4066, 1, 48, 4285, 12, 8, 8, 48, 39, 4066, 0, 49, 45, 0, 45, 49, 1, 48, 23072, 22, 48, 8, 48, 41, 48, 4497, 1, 48, 11867, 11, 8, 8, 48, 39, 4066, 1, 48, 26165, 12, 8, 8, 48, 39, 4066, 55, 1, 48, 38032, 12, 8, 8, 48, 39, 4066, 4, 2, 54, 4330, 3, 3, 5, 0, 1, 48, 12106, 12, 8, 8, 48, 39, 4066, 4, 4, 0, 5, 151, 0, 5, 6, 5, 0, 7, 1, 0, 5, 8, 7, 0, 9, 152, 0, 27, 10, 6, 9, 40, 10, 4778, 0, 11, 153, 0, 5, 8, 11, 26, 12, 6, 8, 40, 12, 4576, 39, 4778, 0, 13, 154, 0, 11, 14, 8, 13, 5, 8, 14, 26, 15, 6, 8, 40, 15, 4596, 39, 4820, 0, 16, 155, 0, 11, 17, 8, 16, 5, 8, 17, 26, 18, 6, 8, 40, 18, 4616, 39, 4903, 0, 19, 156, 0, 11, 20, 8, 19, 5, 8, 20, 26, 21, 6, 8, 40, 21, 4636, 39, 4921, 0, 22, 157, 0, 12, 23, 8, 22, 5, 8, 23, 26, 24, 6, 8, 40, 24, 4656, 39, 4879, 0, 25, 158, 0, 12, 26, 8, 25, 5, 8, 26, 26, 27, 6, 8, 40, 27, 4676, 39, 4844, 0, 28, 159, 0, 11, 29, 8, 28, 5, 8, 29, 26, 30, 6, 8, 40, 30, 4696, 39, 4802, 0, 31, 160, 0, 12, 32, 8, 31, 5, 8, 32, 26, 33, 6, 8, 40, 33, 4716, 39, 4862, 0, 34, 161, 0, 11, 35, 8, 34, 5, 8, 35, 26, 36, 6, 8, 40, 36, 4736, 39, 4811, 0, 37, 162, 0, 11, 38, 8, 37, 5, 8, 38, 26, 39, 6, 8, 40, 39, 4756, 39, 4897, 0, 40, 163, 0, 12, 41, 8, 40, 5, 8, 41, 26, 42, 6, 8, 40, 42, 4776, 39, 4888, 39, 4549, 1, 6, 23644, 3, 44, 1, 0, 45, 150, 0, 42, 46, 44, 1, 45, 1, 43, 30409, 11, 6, 6, 43, 39, 4549, 1, 43, 9452, 12, 6, 6, 43, 39, 4549, 1, 43, 30979, 12, 6, 6, 43, 39, 4549, 1, 6, 37029, 3, 44, 1, 0, 45, 164, 0, 42, 46, 44, 1, 45, 1, 43, 18614, 11, 6, 6, 43, 39, 4549, 3, 44, 0, 42, 45, 44, 1, 0, 55, 1, 43, 39634, 11, 6, 6, 43, 39, 4549, 4, 2, 54, 4820, 3, 54, 4778, 3, 1, 43, 8523, 12, 6, 6, 43, 39, 4549, 1, 43, 2717, 12, 6, 6, 43, 39, 4549, 1, 43, 4269, 11, 6, 6, 43, 39, 4549, 0, 44, 45, 0, 45, 44, 3, 44, 0, 42, 45, 44, 1, 0, 55, 1, 43, 1590, 11, 6, 6, 43, 39, 4549, 3, 44, 1, 0, 45, 165, 0, 42, 46, 44, 1, 45, 1, 43, 2868, 11, 6, 6, 43, 39, 4549, 4, 6, 0, 7, 166, 0, 5, 8, 7, 0, 9, 1, 0, 5, 10, 9, 0, 11, 167, 0, 27, 12, 8, 11, 40, 12, 5127, 0, 13, 166, 0, 5, 10, 13, 26, 14, 8, 10, 40, 14, 4985, 39, 5161, 0, 15, 168, 0, 11, 16, 10, 15, 5, 10, 16, 26, 17, 8, 10, 40, 17, 5005, 39, 5156, 0, 18, 169, 0, 12, 19, 10, 18, 5, 10, 19, 26, 20, 8, 10, 40, 20, 5025, 39, 5180, 0, 21, 170, 0, 11, 22, 10, 21, 5, 10, 22, 26, 23, 8, 10, 40, 23, 5045, 39, 5127, 0, 24, 171, 0, 12, 25, 10, 24, 5, 10, 25, 26, 26, 8, 10, 40, 26, 5065, 39, 5138, 0, 27, 172, 0, 11, 28, 10, 27, 5, 10, 28, 26, 29, 8, 10, 40, 29, 5085, 39, 5237, 0, 30, 173, 0, 11, 31, 10, 30, 5, 10, 31, 26, 32, 8, 10, 40, 32, 5105, 39, 5213, 0, 33, 174, 0, 12, 34, 10, 33, 5, 10, 34, 26, 35, 8, 10, 40, 35, 5125, 39, 5189, 39, 4958, 58, 3, 1, 36, 32088, 11, 8, 8, 36, 39, 4958, 42, 37, 5, 1, 0, 55, 1, 3, 5213, 1, 36, 10637, 11, 8, 8, 36, 39, 4958, 1, 8, 64001, 46, 4, 4, 2, 59, 5189, 3, 4, 5156, 3, 5, 0, 1, 36, 17528, 12, 8, 8, 36, 39, 4958, 1, 36, 1690, 11, 8, 8, 36, 39, 4958, 1, 8, 36062, 3, 37, 1, 0, 38, 175, 0, 42, 39, 37, 1, 38, 1, 36, 4149, 12, 8, 8, 36, 39, 4958, 1, 8, 52554, 3, 37, 1, 0, 38, 43, 0, 42, 39, 37, 1, 38, 1, 36, 14325, 12, 8, 8, 36, 39, 4958, 0, 37, 45, 0, 45, 37, 4, 6, 0, 7, 176, 0, 5, 8, 7, 0, 9, 1, 0, 5, 10, 9, 0, 11, 177, 0, 27, 12, 8, 11, 40, 12, 5468, 0, 13, 178, 0, 5, 10, 13, 26, 14, 8, 10, 40, 14, 5286, 39, 5468, 0, 15, 179, 0, 12, 16, 10, 15, 5, 10, 16, 26, 17, 8, 10, 40, 17, 5306, 39, 5547, 0, 18, 180, 0, 11, 19, 10, 18, 5, 10, 19, 26, 20, 8, 10, 40, 20, 5326, 39, 5597, 0, 21, 181, 0, 11, 22, 10, 21, 5, 10, 22, 26, 23, 8, 10, 40, 23, 5346, 39, 5496, 0, 24, 182, 0, 12, 25, 10, 24, 5, 10, 25, 26, 26, 8, 10, 40, 26, 5366, 39, 5607, 0, 27, 183, 0, 11, 28, 10, 27, 5, 10, 28, 26, 29, 8, 10, 40, 29, 5386, 39, 5505, 0, 30, 184, 0, 11, 31, 10, 30, 5, 10, 31, 26, 32, 8, 10, 40, 32, 5406, 39, 5510, 0, 33, 185, 0, 12, 34, 10, 33, 5, 10, 34, 26, 35, 8, 10, 40, 35, 5426, 39, 5567, 0, 36, 186, 0, 11, 37, 10, 36, 5, 10, 37, 26, 38, 8, 10, 40, 38, 5446, 39, 5578, 0, 39, 187, 0, 12, 40, 10, 39, 5, 10, 40, 26, 41, 8, 10, 40, 41, 5466, 39, 5534, 39, 5259, 1, 42, 57231, 22, 42, 8, 42, 41, 42, 5487, 1, 42, 24954, 11, 8, 8, 42, 39, 5259, 1, 42, 41949, 11, 8, 8, 42, 39, 5259, 1, 8, 37976, 0, 43, 45, 0, 45, 43, 1, 8, 41991, 45, 4, 1, 8, 50281, 3, 43, 1, 0, 44, 175, 0, 42, 45, 43, 1, 44, 1, 42, 47320, 12, 8, 8, 42, 39, 5259, 55, 1, 3, 5496, 1, 42, 9289, 11, 8, 8, 42, 39, 5259, 42, 43, 5, 1, 0, 5, 4, 43, 1, 3, 5505, 1, 42, 11810, 11, 8, 8, 42, 39, 5259, 58, 3, 1, 42, 2597, 12, 8, 8, 42, 39, 5259, 4, 2, 59, 5510, 3, 4, 5607, 3, 5, 0, 1, 42, 51851, 12, 8, 8, 42, 39, 5259, 55, 1, 42, 35351, 11, 8, 8, 42, 39, 5259, 1, 8, 364, 46, 4, 4, 8, 0, 9, 188, 0, 5, 10, 9, 0, 11, 1, 0, 5, 12, 11, 0, 13, 189, 0, 27, 14, 10, 13, 40, 14, 5857, 0, 15, 190, 0, 5, 12, 15, 26, 16, 10, 12, 40, 16, 5655, 39, 5857, 0, 17, 191, 0, 11, 18, 12, 17, 5, 12, 18, 26, 19, 10, 12, 40, 19, 5675, 39, 5970, 0, 20, 192, 0, 12, 21, 12, 20, 5, 12, 21, 26, 22, 10, 12, 40, 22, 5695, 39, 5903, 0, 23, 193, 0, 11, 24, 12, 23, 5, 12, 24, 26, 25, 10, 12, 40, 25, 5715, 39, 5989, 0, 26, 194, 0, 11, 27, 12, 26, 5, 12, 27, 26, 28, 10, 12, 40, 28, 5735, 39, 5866, 0, 29, 195, 0, 12, 30, 12, 29, 5, 12, 30, 26, 31, 10, 12, 40, 31, 5755, 39, 5946, 0, 32, 196, 0, 11, 33, 12, 32, 5, 12, 33, 26, 34, 10, 12, 40, 34, 5775, 39, 5926, 0, 35, 197, 0, 12, 36, 12, 35, 5, 12, 36, 26, 37, 10, 12, 40, 37, 5795, 39, 5898, 0, 38, 198, 0, 11, 39, 12, 38, 5, 12, 39, 26, 40, 10, 12, 40, 40, 5815, 39, 5881, 0, 41, 199, 0, 12, 42, 12, 41, 5, 12, 42, 26, 43, 10, 12, 40, 43, 5835, 39, 5959, 0, 44, 200, 0, 12, 45, 12, 44, 5, 12, 45, 26, 46, 10, 12, 40, 46, 5855, 39, 6017, 39, 5628, 1, 47, 7561, 11, 10, 10, 47, 39, 5628, 42, 48, 7, 1, 0, 55, 1, 47, 12879, 12, 10, 10, 47, 39, 5628, 1, 10, 12570, 3, 48, 1, 42, 49, 48, 1, 3, 0, 48, 45, 0, 45, 48, 1, 10, 6372, 46, 6, 1, 10, 10812, 0, 48, 201, 0, 11, 49, 3, 48, 5, 3, 49, 1, 47, 1778, 12, 10, 10, 47, 39, 5628, 59, 5903, 5, 6, 5898, 54, 5970, 4, 3, 7, 0, 1, 47, 9830, 11, 10, 10, 47, 39, 5628, 55, 1, 5, 5881, 1, 47, 12772, 12, 10, 10, 47, 39, 5628, 58, 5, 1, 47, 2662, 12, 10, 10, 47, 39, 5628, 1, 10, 36493, 0, 48, 40, 0, 5, 3, 48, 1, 47, 12909, 12, 10, 10, 47, 39, 5628, 1, 47, 46673, 22, 47, 10, 47, 41, 47, 6008, 1, 47, 6371, 11, 10, 10, 47, 39, 5628, 1, 47, 11749, 12, 10, 10, 47, 39, 5628, 4, 2, 0, 48, 1, 0, 5, 3, 48, 1, 47, 20718, 11, 10, 10, 47, 39, 5628, 4, 4, 0, 5, 202, 0, 5, 6, 5, 0, 7, 1, 0, 5, 8, 7, 0, 9, 203, 0, 27, 10, 6, 9, 40, 10, 6240, 0, 11, 26, 0, 5, 8, 11, 26, 12, 6, 8, 40, 12, 6078, 39, 6332, 0, 13, 204, 0, 12, 14, 8, 13, 5, 8, 14, 26, 15, 6, 8, 40, 15, 6098, 39, 6323, 0, 16, 205, 0, 11, 17, 8, 16, 5, 8, 17, 26, 18, 6, 8, 40, 18, 6118, 39, 6257, 0, 19, 206, 0, 12, 20, 8, 19, 5, 8, 20, 26, 21, 6, 8, 40, 21, 6138, 39, 6357, 0, 22, 207, 0, 12, 23, 8, 22, 5, 8, 23, 26, 24, 6, 8, 40, 24, 6158, 39, 6286, 0, 25, 208, 0, 11, 26, 8, 25, 5, 8, 26, 26, 27, 6, 8, 40, 27, 6178, 39, 6338, 0, 28, 209, 0, 12, 29, 8, 28, 5, 8, 29, 26, 30, 6, 8, 40, 30, 6198, 39, 6295, 0, 31, 210, 0, 11, 32, 8, 31, 5, 8, 32, 26, 33, 6, 8, 40, 33, 6218, 39, 6240, 0, 34, 211, 0, 12, 35, 8, 34, 5, 8, 35, 26, 36, 6, 8, 40, 36, 6238, 39, 6277, 39, 6051, 4, 2, 54, 6338, 3, 3, 38, 0, 1, 37, 18573, 11, 6, 6, 37, 39, 6051, 3, 38, 1, 3, 39, 0, 42, 40, 38, 1, 39, 1, 37, 41947, 12, 6, 6, 37, 39, 6051, 1, 37, 34880, 11, 6, 6, 37, 39, 6051, 1, 37, 51723, 11, 6, 6, 37, 39, 6051, 1, 37, 37040, 22, 37, 6, 37, 41, 37, 6314, 1, 37, 8621, 12, 6, 6, 37, 39, 6051, 1, 37, 17457, 11, 6, 6, 37, 39, 6051, 1, 37, 0, 11, 6, 6, 37, 39, 6051, 0, 38, 45, 0, 45, 38, 1, 6, 27669, 0, 38, 212, 0, 7, 0, 38, 1, 37, 28228, 11, 6, 6, 37, 39, 6051, 55, 1, 37, 9782, 11, 6, 6, 37, 39, 6051, 4, 5, 0, 6, 213, 0, 5, 7, 6, 0, 8, 1, 0, 5, 9, 8, 0, 10, 214, 0, 27, 11, 7, 10, 40, 11, 6552, 0, 12, 215, 0, 5, 9, 12, 26, 13, 7, 9, 40, 13, 6410, 39, 6677, 0, 14, 216, 0, 11, 15, 9, 14, 5, 9, 15, 26, 16, 7, 9, 40, 16, 6430, 39, 6570, 0, 17, 217, 0, 12, 18, 9, 17, 5, 9, 18, 26, 19, 7, 9, 40, 19, 6450, 39, 6696, 0, 20, 218, 0, 12, 21, 9, 20, 5, 9, 21, 26, 22, 7, 9, 40, 22, 6470, 39, 6579, 0, 23, 219, 0, 11, 24, 9, 23, 5, 9, 24, 26, 25, 7, 9, 40, 25, 6490, 39, 6552, 0, 26, 220, 0, 12, 27, 9, 26, 5, 9, 27, 26, 28, 7, 9, 40, 28, 6510, 39, 6593, 0, 29, 221, 0, 11, 30, 9, 29, 5, 9, 30, 26, 31, 7, 9, 40, 31, 6530, 39, 6621, 0, 32, 222, 0, 11, 33, 9, 32, 5, 9, 33, 26, 34, 7, 9, 40, 34, 6550, 39, 6649, 39, 6383, 4, 2, 0, 36, 43, 0, 5, 3, 36, 1, 35, 1286, 11, 7, 7, 35, 39, 6383, 1, 35, 2896, 12, 7, 7, 35, 39, 6383, 3, 36, 0, 42, 37, 36, 1, 3, 0, 36, 45, 0, 45, 36, 1, 35, 44861, 22, 35, 7, 35, 41, 35, 6612, 1, 35, 23735, 11, 7, 7, 35, 39, 6383, 1, 35, 1144, 11, 7, 7, 35, 39, 6383, 1, 35, 51164, 22, 35, 7, 35, 41, 35, 6640, 1, 35, 24594, 11, 7, 7, 35, 39, 6383, 1, 35, 24594, 11, 7, 7, 35, 39, 6383, 1, 35, 2118, 22, 35, 7, 35, 41, 35, 6668, 1, 35, 0, 11, 7, 7, 35, 39, 6383, 1, 35, 30729, 12, 7, 7, 35, 39, 6383, 1, 7, 22563, 0, 36, 223, 0, 5, 3, 36, 1, 35, 11827, 11, 7, 7, 35, 39, 6383, 54, 6677, 4, 55, 1, 35, 12050, 12, 7, 7, 35, 39, 6383, 4, 6, 0, 7, 224, 0, 5, 8, 7, 0, 9, 1, 0, 5, 10, 9, 0, 11, 225, 0, 27, 12, 8, 11, 40, 12, 6894, 0, 13, 226, 0, 5, 10, 13, 26, 14, 8, 10, 40, 14, 6752, 39, 7019, 0, 15, 227, 0, 12, 16, 10, 15, 5, 10, 16, 26, 17, 8, 10, 40, 17, 6772, 39, 6986, 0, 18, 228, 0, 12, 19, 10, 18, 5, 10, 19, 26, 20, 8, 10, 40, 20, 6792, 39, 6951, 0, 21, 229, 0, 11, 22, 10, 21, 5, 10, 22, 26, 23, 8, 10, 40, 23, 6812, 39, 6968, 0, 24, 230, 0, 12, 25, 10, 24, 5, 10, 25, 26, 26, 8, 10, 40, 26, 6832, 39, 7010, 0, 27, 231, 0, 12, 28, 10, 27, 5, 10, 28, 26, 29, 8, 10, 40, 29, 6852, 39, 6894, 0, 30, 232, 0, 11, 31, 10, 30, 5, 10, 31, 26, 32, 8, 10, 40, 32, 6872, 39, 6933, 0, 33, 233, 0, 12, 34, 10, 33, 5, 10, 34, 26, 35, 8, 10, 40, 35, 6892, 39, 6922, 39, 6725, 1, 36, 29373, 22, 36, 8, 36, 41, 36, 6913, 1, 36, 9056, 11, 8, 8, 36, 39, 6725, 1, 36, 0, 11, 8, 8, 36, 39, 6725, 58, 4, 1, 36, 29135, 11, 8, 8, 36, 39, 6725, 4, 2, 0, 37, 96, 0, 5, 3, 37, 1, 36, 14174, 12, 8, 8, 36, 39, 6725, 1, 8, 11403, 3, 37, 0, 42, 38, 37, 1, 3, 0, 37, 45, 0, 45, 37, 59, 6986, 4, 5, 7019, 55, 1, 4, 6951, 1, 36, 12428, 12, 8, 8, 36, 39, 6725, 1, 8, 36499, 3, 37, 0, 0, 38, 175, 0, 42, 39, 37, 1, 38, 1, 36, 24706, 12, 8, 8, 36, 39, 6725, 1, 36, 23758, 11, 8, 8, 36, 39, 6725, 1, 8, 40928, 46, 5, 4, 7, 0, 8, 234, 0, 5, 9, 8, 0, 10, 1, 0, 5, 11, 10, 0, 12, 235, 0, 27, 13, 9, 12, 40, 13, 7309, 0, 14, 236, 0, 5, 11, 14, 26, 15, 9, 11, 40, 15, 7067, 39, 7309, 0, 16, 237, 0, 11, 17, 11, 16, 5, 11, 17, 26, 18, 9, 11, 40, 18, 7087, 39, 7501, 0, 19, 238, 0, 11, 20, 11, 19, 5, 11, 20, 26, 21, 9, 11, 40, 21, 7107, 39, 7343, 0, 22, 239, 0, 12, 23, 11, 22, 5, 11, 23, 26, 24, 9, 11, 40, 24, 7127, 39, 7499, 0, 25, 240, 0, 11, 26, 11, 25, 5, 11, 26, 26, 27, 9, 11, 40, 27, 7147, 39, 7395, 0, 28, 241, 0, 12, 29, 11, 28, 5, 11, 29, 26, 30, 9, 11, 40, 30, 7167, 39, 7510, 0, 31, 242, 0, 11, 32, 11, 31, 5, 11, 32, 26, 33, 9, 11, 40, 33, 7187, 39, 7436, 0, 34, 243, 0, 12, 35, 11, 34, 5, 11, 35, 26, 36, 9, 11, 40, 36, 7207, 39, 7481, 0, 37, 244, 0, 11, 38, 11, 37, 5, 11, 38, 26, 39, 9, 11, 40, 39, 7227, 39, 7460, 0, 40, 245, 0, 11, 41, 11, 40, 5, 11, 41, 26, 42, 9, 11, 40, 42, 7247, 39, 7423, 0, 43, 246, 0, 11, 44, 11, 43, 5, 11, 44, 26, 45, 9, 11, 40, 45, 7267, 39, 7315, 0, 46, 247, 0, 11, 47, 11, 46, 5, 11, 47, 26, 48, 9, 11, 40, 48, 7287, 39, 7367, 0, 49, 248, 0, 12, 50, 11, 49, 5, 11, 50, 26, 51, 9, 11, 40, 51, 7307, 39, 7519, 39, 7040, 0, 53, 45, 0, 45, 53, 1, 52, 4891, 22, 52, 9, 52, 41, 52, 7334, 1, 52, 13145, 12, 9, 9, 52, 39, 7040, 1, 52, 8153, 12, 9, 9, 52, 39, 7040, 1, 9, 39963, 0, 53, 249, 0, 8, 5, 4, 53, 2, 6, 250, 0, 1, 52, 37034, 12, 9, 9, 52, 39, 7040, 1, 52, 6139, 22, 52, 9, 52, 41, 52, 7386, 1, 52, 46955, 12, 9, 9, 52, 39, 7040, 1, 52, 0, 11, 9, 9, 52, 39, 7040, 1, 52, 8385, 22, 52, 9, 52, 41, 52, 7414, 1, 52, 15573, 12, 9, 9, 52, 39, 7040, 1, 52, 35409, 12, 9, 9, 52, 39, 7040, 2, 53, 251, 0, 1, 52, 5772, 11, 9, 9, 52, 39, 7040, 54, 7343, 4, 41, 0, 7451, 1, 52, 45864, 12, 9, 9, 52, 39, 7040, 1, 52, 51636, 12, 9, 9, 52, 39, 7040, 31, 53, 4, 6, 48, 54, 2, 5, 53, 5, 3, 54, 1, 52, 20573, 11, 9, 9, 52, 39, 7040, 4, 2, 0, 53, 252, 0, 5, 3, 53, 1, 52, 58534, 11, 9, 9, 52, 39, 7040, 45, 3, 1, 52, 11890, 12, 9, 9, 52, 39, 7040, 1, 52, 5635, 12, 9, 9, 52, 39, 7040, 55, 1, 52, 8110, 11, 9, 9, 52, 39, 7040, 4, 10, 0, 11, 253, 0, 5, 12, 11, 0, 13, 1, 0, 5, 14, 13, 0, 15, 254, 0, 27, 16, 12, 15, 40, 16, 8054, 0, 17, 255, 0, 5, 14, 17, 26, 18, 12, 14, 40, 18, 7572, 39, 8347, 0, 19, 256, 0, 12, 20, 14, 19, 5, 14, 20, 26, 21, 12, 14, 40, 21, 7592, 39, 8097, 0, 22, 257, 0, 11, 23, 14, 22, 5, 14, 23, 26, 24, 12, 14, 40, 24, 7612, 39, 8239, 0, 25, 258, 0, 11, 26, 14, 25, 5, 14, 26, 26, 27, 12, 14, 40, 27, 7632, 39, 8138, 0, 28, 259, 0, 12, 29, 14, 28, 5, 14, 29, 26, 30, 12, 14, 40, 30, 7652, 39, 8227, 0, 31, 260, 0, 12, 32, 14, 31, 5, 14, 32, 26, 33, 12, 14, 40, 33, 7672, 39, 8054, 0, 34, 261, 0, 11, 35, 14, 34, 5, 14, 35, 26, 36, 12, 14, 40, 36, 7692, 39, 8367, 0, 37, 262, 0, 11, 38, 14, 37, 5, 14, 38, 26, 39, 12, 14, 40, 39, 7712, 39, 8342, 0, 40, 263, 0, 12, 41, 14, 40, 5, 14, 41, 26, 42, 12, 14, 40, 42, 7732, 39, 8306, 0, 43, 264, 0, 12, 44, 14, 43, 5, 14, 44, 26, 45, 12, 14, 40, 45, 7752, 39, 8293, 0, 46, 265, 0, 11, 47, 14, 46, 5, 14, 47, 26, 48, 12, 14, 40, 48, 7772, 39, 8252, 0, 49, 266, 0, 11, 50, 14, 49, 5, 14, 50, 26, 51, 12, 14, 40, 51, 7792, 39, 8081, 0, 52, 267, 0, 11, 53, 14, 52, 5, 14, 53, 26, 54, 12, 14, 40, 54, 7812, 39, 8175, 0, 55, 268, 0, 12, 56, 14, 55, 5, 14, 56, 26, 57, 12, 14, 40, 57, 7832, 39, 8214, 0, 58, 269, 0, 11, 59, 14, 58, 5, 14, 59, 26, 60, 12, 14, 40, 60, 7852, 39, 8208, 0, 61, 270, 0, 12, 62, 14, 61, 5, 14, 62, 26, 63, 12, 14, 40, 63, 7872, 39, 8264, 0, 64, 271, 0, 12, 65, 14, 64, 5, 14, 65, 26, 66, 12, 14, 40, 66, 7892, 39, 8063, 0, 67, 272, 0, 12, 68, 14, 67, 5, 14, 68, 26, 69, 12, 14, 40, 69, 7912, 39, 8216, 0, 70, 273, 0, 11, 71, 14, 70, 5, 14, 71, 26, 72, 12, 14, 40, 72, 7932, 39, 8387, 0, 73, 274, 0, 11, 74, 14, 73, 5, 14, 74, 26, 75, 12, 14, 40, 75, 7952, 39, 8196, 0, 76, 275, 0, 12, 77, 14, 76, 5, 14, 77, 26, 78, 12, 14, 40, 78, 7972, 39, 8421, 0, 79, 276, 0, 11, 80, 14, 79, 5, 14, 80, 26, 81, 12, 14, 40, 81, 7992, 39, 8147, 0, 82, 277, 0, 11, 83, 14, 82, 5, 14, 83, 26, 84, 12, 14, 40, 84, 8012, 39, 8449, 0, 85, 278, 0, 12, 86, 14, 85, 5, 14, 86, 26, 87, 12, 14, 40, 87, 8032, 39, 8126, 0, 88, 279, 0, 11, 89, 14, 88, 5, 14, 89, 26, 90, 12, 14, 40, 90, 8052, 39, 8329, 39, 7545, 1, 91, 16103, 11, 12, 12, 91, 39, 7545, 4, 2, 0, 92, 1, 0, 5, 3, 92, 1, 91, 29883, 11, 12, 12, 91, 39, 7545, 0, 92, 1, 0, 5, 4, 92, 1, 91, 28284, 12, 12, 12, 91, 39, 7545, 0, 92, 97, 0, 22, 93, 4, 92, 41, 93, 8117, 1, 91, 15463, 11, 12, 12, 91, 39, 7545, 1, 91, 24657, 11, 12, 12, 91, 39, 7545, 5, 4, 9, 1, 91, 4445, 11, 12, 12, 91, 39, 7545, 1, 91, 0, 11, 12, 12, 91, 39, 7545, 1, 91, 1161, 22, 91, 12, 91, 41, 91, 8166, 1, 91, 15776, 12, 12, 12, 91, 39, 7545, 1, 91, 21919, 12, 12, 12, 91, 39, 7545, 42, 92, 7, 1, 8, 11, 92, 3, 4, 5, 3, 92, 1, 91, 45304, 12, 12, 12, 91, 39, 7545, 1, 12, 52550, 1, 91, 12639, 12, 12, 12, 91, 39, 7545, 0, 92, 45, 0, 45, 92, 45, 3, 58, 5, 1, 91, 20678, 11, 12, 12, 91, 39, 7545, 1, 12, 30406, 1, 91, 18414, 11, 12, 12, 91, 39, 7545, 1, 5, 8196, 55, 1, 91, 19779, 12, 12, 12, 91, 39, 7545, 1, 12, 19076, 1, 91, 29744, 11, 12, 12, 91, 39, 7545, 0, 92, 96, 0, 26, 93, 4, 92, 41, 93, 8284, 1, 91, 29146, 11, 12, 12, 91, 39, 7545, 1, 91, 11152, 11, 12, 12, 91, 39, 7545, 55, 1, 5, 8227, 1, 91, 9818, 11, 12, 12, 91, 39, 7545, 1, 12, 27297, 0, 92, 201, 0, 11, 93, 3, 92, 5, 3, 93, 1, 91, 5013, 12, 12, 12, 91, 39, 7545, 1, 5, 8252, 55, 1, 91, 23481, 12, 12, 12, 91, 39, 7545, 1, 12, 42962, 46, 6, 5, 92, 4, 0, 92, 40, 0, 11, 9, 4, 92, 1, 91, 28817, 12, 12, 12, 91, 39, 7545, 0, 92, 43, 0, 26, 93, 4, 92, 5, 8, 93, 1, 91, 45651, 11, 12, 12, 91, 39, 7545, 59, 8306, 5, 6, 8342, 0, 92, 40, 0, 26, 93, 4, 92, 41, 93, 8412, 1, 91, 13181, 12, 12, 12, 91, 39, 7545, 1, 91, 1673, 11, 12, 12, 91, 39, 7545, 1, 91, 38529, 22, 91, 12, 91, 41, 91, 8440, 1, 91, 36905, 11, 12, 12, 91, 39, 7545, 1, 91, 0, 11, 12, 12, 91, 39, 7545, 3, 7, 0, 5, 8, 0, 41, 8, 8467, 1, 91, 2287, 12, 12, 12, 91, 39, 7545, 1, 91, 47938, 12, 12, 12, 91, 39, 7545, 4, 9, 0, 10, 280, 0, 5, 11, 10, 0, 12, 1, 0, 5, 13, 12, 0, 14, 281, 0, 27, 15, 11, 14, 40, 15, 8901, 0, 16, 282, 0, 5, 13, 16, 26, 17, 11, 13, 40, 17, 8519, 39, 9067, 0, 18, 283, 0, 11, 19, 13, 18, 5, 13, 19, 26, 20, 11, 13, 40, 20, 8539, 39, 8986, 0, 21, 284, 0, 12, 22, 13, 21, 5, 13, 22, 26, 23, 11, 13, 40, 23, 8559, 39, 8911, 0, 24, 285, 0, 11, 25, 13, 24, 5, 13, 25, 26, 26, 11, 13, 40, 26, 8579, 39, 9044, 0, 27, 286, 0, 11, 28, 13, 27, 5, 13, 28, 26, 29, 11, 13, 40, 29, 8599, 39, 9028, 0, 30, 287, 0, 12, 31, 13, 30, 5, 13, 31, 26, 32, 11, 13, 40, 32, 8619, 39, 9168, 0, 33, 288, 0, 11, 34, 13, 33, 5, 13, 34, 26, 35, 11, 13, 40, 35, 8639, 39, 9170, 0, 36, 289, 0, 12, 37, 13, 36, 5, 13, 37, 26, 38, 11, 13, 40, 38, 8659, 39, 9220, 0, 39, 290, 0, 12, 40, 13, 39, 5, 13, 40, 26, 41, 11, 13, 40, 41, 8679, 39, 8996, 0, 42, 291, 0, 11, 43, 13, 42, 5, 13, 43, 26, 44, 11, 13, 40, 44, 8699, 39, 9095, 0, 45, 292, 0, 11, 46, 13, 45, 5, 13, 46, 26, 47, 11, 13, 40, 47, 8719, 39, 9150, 0, 48, 293, 0, 12, 49, 13, 48, 5, 13, 49, 26, 50, 11, 13, 40, 50, 8739, 39, 8901, 0, 51, 294, 0, 12, 52, 13, 51, 5, 13, 52, 26, 53, 11, 13, 40, 53, 8759, 39, 9199, 0, 54, 295, 0, 11, 55, 13, 54, 5, 13, 55, 26, 56, 11, 13, 40, 56, 8779, 39, 9159, 0, 57, 296, 0, 12, 58, 13, 57, 5, 13, 58, 26, 59, 11, 13, 40, 59, 8799, 39, 9238, 0, 60, 297, 0, 11, 61, 13, 60, 5, 13, 61, 26, 62, 11, 13, 40, 62, 8819, 39, 9123, 0, 63, 298, 0, 11, 64, 13, 63, 5, 13, 64, 26, 65, 11, 13, 40, 65, 8839, 39, 8931, 0, 66, 299, 0, 12, 67, 13, 66, 5, 13, 67, 26, 68, 11, 13, 40, 68, 8859, 39, 8958, 0, 69, 300, 0, 11, 70, 13, 69, 5, 13, 70, 26, 71, 11, 13, 40, 71, 8879, 39, 9208, 0, 72, 301, 0, 11, 73, 13, 72, 5, 13, 73, 26, 74, 11, 13, 40, 74, 8899, 39, 9129, 39, 8492, 55, 1, 75, 29155, 12, 11, 11, 75, 39, 8492, 5, 76, 4, 0, 76, 40, 0, 11, 8, 4, 76, 1, 75, 25811, 11, 11, 11, 75, 39, 8492, 3, 6, 0, 5, 7, 0, 41, 7, 8949, 1, 75, 264, 11, 11, 11, 75, 39, 8492, 1, 75, 28433, 12, 11, 11, 75, 39, 8492, 1, 75, 7882, 22, 75, 11, 75, 41, 75, 8977, 1, 75, 31725, 11, 11, 11, 75, 39, 8492, 1, 75, 7537, 11, 11, 11, 75, 39, 8492, 55, 1, 75, 15883, 11, 11, 11, 75, 39, 8492, 54, 9044, 5, 0, 76, 96, 0, 26, 77, 4, 76, 41, 77, 9019, 1, 75, 39378, 11, 11, 11, 75, 39, 8492, 1, 75, 40865, 11, 11, 11, 75, 39, 8492, 0, 76, 1, 0, 5, 4, 76, 1, 75, 5389, 12, 11, 11, 75, 39, 8492, 1, 11, 36457, 0, 76, 302, 0, 11, 77, 3, 76, 5, 3, 77, 1, 75, 21189, 11, 11, 11, 75, 39, 8492, 1, 75, 43159, 22, 75, 11, 75, 41, 75, 9086, 1, 75, 31450, 11, 11, 11, 75, 39, 8492, 1, 75, 30121, 11, 11, 11, 75, 39, 8492, 1, 75, 54010, 22, 75, 11, 75, 41, 75, 9114, 1, 75, 1928, 12, 11, 11, 75, 39, 8492, 1, 75, 0, 11, 11, 11, 75, 39, 8492, 0, 76, 45, 0, 45, 76, 42, 76, 6, 1, 7, 11, 76, 3, 4, 5, 3, 76, 1, 75, 4374, 12, 11, 11, 75, 39, 8492, 1, 75, 39408, 12, 11, 11, 75, 39, 8492, 1, 75, 5185, 12, 11, 11, 75, 39, 8492, 45, 3, 0, 76, 97, 0, 22, 77, 4, 76, 41, 77, 9190, 1, 75, 25095, 12, 11, 11, 75, 39, 8492, 1, 75, 36805, 12, 11, 11, 75, 39, 8492, 1, 75, 12278, 11, 11, 11, 75, 39, 8492, 5, 4, 8, 1, 75, 749, 12, 11, 11, 75, 39, 8492, 4, 2, 0, 76, 1, 0, 5, 3, 76, 1, 75, 36393, 11, 11, 11, 75, 39, 8492, 0, 76, 40, 0, 26, 77, 4, 76, 5, 7, 77, 1, 75, 28697, 11, 11, 11, 75, 39, 8492, 4, 9, 0, 10, 303, 0, 5, 11, 10, 0, 12, 1, 0, 5, 13, 12, 0, 14, 304, 0, 27, 15, 11, 14, 40, 15, 9643, 0, 16, 305, 0, 5, 13, 16, 26, 17, 11, 13, 40, 17, 9301, 39, 9887, 0, 18, 306, 0, 12, 19, 13, 18, 5, 13, 19, 26, 20, 11, 13, 40, 20, 9321, 39, 9841, 0, 21, 307, 0, 11, 22, 13, 21, 5, 13, 22, 26, 23, 11, 13, 40, 23, 9341, 39, 9716, 0, 24, 308, 0, 12, 25, 13, 24, 5, 13, 25, 26, 26, 11, 13, 40, 26, 9361, 39, 9859, 0, 27, 309, 0, 11, 28, 13, 27, 5, 13, 28, 26, 29, 11, 13, 40, 29, 9381, 39, 9686, 0, 30, 310, 0, 12, 31, 13, 30, 5, 13, 31, 26, 32, 11, 13, 40, 32, 9401, 39, 9643, 0, 33, 311, 0, 12, 34, 13, 33, 5, 13, 34, 26, 35, 11, 13, 40, 35, 9421, 39, 9802, 0, 36, 312, 0, 11, 37, 13, 36, 5, 13, 37, 26, 38, 11, 13, 40, 38, 9441, 39, 9906, 0, 39, 313, 0, 11, 40, 13, 39, 5, 13, 40, 26, 41, 11, 13, 40, 41, 9461, 39, 9811, 0, 42, 314, 0, 11, 43, 13, 42, 5, 13, 43, 26, 44, 11, 13, 40, 44, 9481, 39, 9912, 0, 45, 315, 0, 12, 46, 13, 45, 5, 13, 46, 26, 47, 11, 13, 40, 47, 9501, 39, 9695, 0, 48, 316, 0, 11, 49, 13, 48, 5, 13, 49, 26, 50, 11, 13, 40, 50, 9521, 39, 9745, 0, 51, 317, 0, 11, 52, 13, 51, 5, 13, 52, 26, 53, 11, 13, 40, 53, 9541, 39, 9765, 0, 54, 318, 0, 12, 55, 13, 54, 5, 13, 55, 26, 56, 11, 13, 40, 56, 9561, 39, 9897, 0, 57, 319, 0, 11, 58, 13, 57, 5, 13, 58, 26, 59, 11, 13, 40, 59, 9581, 39, 9663, 0, 60, 320, 0, 12, 61, 13, 60, 5, 13, 61, 26, 62, 11, 13, 40, 62, 9601, 39, 9928, 0, 63, 321, 0, 11, 64, 13, 63, 5, 13, 64, 26, 65, 11, 13, 40, 65, 9621, 39, 9774, 0, 66, 322, 0, 12, 67, 13, 66, 5, 13, 67, 26, 68, 11, 13, 40, 68, 9641, 39, 9930, 39, 9274, 5, 70, 4, 0, 70, 40, 0, 11, 8, 4, 70, 1, 69, 4586, 12, 11, 11, 69, 39, 9274, 1, 11, 52470, 0, 70, 302, 0, 11, 71, 3, 70, 5, 3, 71, 1, 69, 26053, 12, 11, 11, 69, 39, 9274, 1, 69, 20968, 12, 11, 11, 69, 39, 9274, 42, 70, 6, 1, 7, 11, 70, 3, 4, 5, 3, 70, 1, 69, 10931, 11, 11, 11, 69, 39, 9274, 0, 70, 97, 0, 22, 71, 4, 70, 41, 71, 9736, 1, 69, 9089, 12, 11, 11, 69, 39, 9274, 1, 69, 1801, 12, 11, 11, 69, 39, 9274, 0, 70, 43, 0, 26, 71, 4, 70, 5, 7, 71, 1, 69, 39328, 12, 11, 11, 69, 39, 9274, 1, 69, 20534, 12, 11, 11, 69, 39, 9274, 1, 69, 8104, 22, 69, 11, 69, 41, 69, 9793, 1, 69, 13309, 12, 11, 11, 69, 39, 9274, 1, 69, 5858, 11, 11, 11, 69, 39, 9274, 1, 69, 10578, 11, 11, 11, 69, 39, 9274, 54, 9663, 5, 3, 6, 0, 5, 7, 0, 41, 7, 9832, 1, 69, 21187, 12, 11, 11, 69, 39, 9274, 1, 69, 18141, 11, 11, 11, 69, 39, 9274, 4, 2, 0, 70, 1, 0, 5, 3, 70, 1, 69, 51828, 11, 11, 11, 69, 39, 9274, 1, 69, 8835, 22, 69, 11, 69, 41, 69, 9878, 1, 69, 18132, 11, 11, 11, 69, 39, 9274, 1, 69, 12552, 11, 11, 11, 69, 39, 9274, 55, 1, 69, 1102, 12, 11, 11, 69, 39, 9274, 1, 69, 15553, 11, 11, 11, 69, 39, 9274, 0, 70, 45, 0, 45, 70, 0, 70, 1, 0, 5, 4, 70, 1, 69, 24444, 12, 11, 11, 69, 39, 9274, 45, 3, 5, 4, 8, 1, 69, 7167, 11, 11, 11, 69, 39, 9274, 4, 9, 0, 10, 323, 0, 5, 11, 10, 0, 12, 1, 0, 5, 13, 12, 0, 14, 324, 0, 27, 15, 11, 14, 40, 15, 10327, 0, 16, 325, 0, 5, 13, 16, 26, 17, 11, 13, 40, 17, 9985, 39, 10526, 0, 18, 326, 0, 11, 19, 13, 18, 5, 13, 19, 26, 20, 11, 13, 40, 20, 10005, 39, 10327, 0, 21, 327, 0, 12, 22, 13, 21, 5, 13, 22, 26, 23, 11, 13, 40, 23, 10025, 39, 10403, 0, 24, 328, 0, 11, 25, 13, 24, 5, 13, 25, 26, 26, 11, 13, 40, 26, 10045, 39, 10535, 0, 27, 329, 0, 12, 28, 13, 27, 5, 13, 28, 26, 29, 11, 13, 40, 29, 10065, 39, 10482, 0, 30, 330, 0, 12, 31, 13, 30, 5, 13, 31, 26, 32, 11, 13, 40, 32, 10085, 39, 10343, 0, 33, 331, 0, 11, 34, 13, 33, 5, 13, 34, 26, 35, 11, 13, 40, 35, 10105, 39, 10393, 0, 36, 332, 0, 12, 37, 13, 36, 5, 13, 37, 26, 38, 11, 13, 40, 38, 10125, 39, 10384, 0, 39, 333, 0, 12, 40, 13, 39, 5, 13, 40, 26, 41, 11, 13, 40, 41, 10145, 39, 10473, 0, 42, 334, 0, 11, 43, 13, 42, 5, 13, 43, 26, 44, 11, 13, 40, 44, 10165, 39, 10576, 0, 45, 335, 0, 11, 46, 13, 45, 5, 13, 46, 26, 47, 11, 13, 40, 47, 10185, 39, 10366, 0, 48, 336, 0, 12, 49, 13, 48, 5, 13, 49, 26, 50, 11, 13, 40, 50, 10205, 39, 10453, 0, 51, 337, 0, 11, 52, 13, 51, 5, 13, 52, 26, 53, 11, 13, 40, 53, 10225, 39, 10433, 0, 54, 338, 0, 12, 55, 13, 54, 5, 13, 55, 26, 56, 11, 13, 40, 56, 10245, 39, 10345, 0, 57, 339, 0, 11, 58, 13, 57, 5, 13, 58, 26, 59, 11, 13, 40, 59, 10265, 39, 10488, 0, 60, 340, 0, 12, 61, 13, 60, 5, 13, 61, 26, 62, 11, 13, 40, 62, 10285, 39, 10517, 0, 63, 341, 0, 11, 64, 13, 63, 5, 13, 64, 26, 65, 11, 13, 40, 65, 10305, 39, 10553, 0, 66, 342, 0, 12, 67, 13, 66, 5, 13, 67, 26, 68, 11, 13, 40, 68, 10325, 39, 10375, 39, 9958, 0, 70, 1, 0, 5, 4, 70, 1, 69, 31703, 12, 11, 11, 69, 39, 9958, 45, 3, 42, 70, 6, 1, 7, 11, 70, 3, 4, 5, 3, 70, 1, 69, 8906, 11, 11, 11, 69, 39, 9958, 1, 69, 52979, 12, 11, 11, 69, 39, 9958, 1, 69, 20390, 12, 11, 11, 69, 39, 9958, 1, 69, 0, 11, 11, 11, 69, 39, 9958, 55, 1, 69, 10002, 12, 11, 11, 69, 39, 9958, 54, 10553, 5, 3, 6, 0, 5, 7, 0, 41, 7, 10424, 1, 69, 17704, 11, 11, 11, 69, 39, 9958, 1, 69, 45706, 11, 11, 11, 69, 39, 9958, 0, 70, 40, 0, 26, 71, 4, 70, 5, 7, 71, 1, 69, 28002, 12, 11, 11, 69, 39, 9958, 5, 70, 4, 0, 70, 40, 0, 11, 8, 4, 70, 1, 69, 10485, 12, 11, 11, 69, 39, 9958, 1, 69, 25409, 11, 11, 11, 69, 39, 9958, 0, 70, 45, 0, 45, 70, 0, 70, 96, 0, 22, 71, 4, 70, 41, 71, 10508, 1, 69, 16689, 12, 11, 11, 69, 39, 9958, 1, 69, 10244, 12, 11, 11, 69, 39, 9958, 1, 69, 18282, 11, 11, 11, 69, 39, 9958, 1, 69, 24477, 12, 11, 11, 69, 39, 9958, 4, 2, 0, 70, 1, 0, 5, 3, 70, 1, 69, 25858, 12, 11, 11, 69, 39, 9958, 1, 11, 49379, 0, 70, 302, 0, 11, 71, 3, 70, 5, 3, 71, 1, 69, 29469, 12, 11, 11, 69, 39, 9958, 5, 4, 8, 1, 69, 28598, 11, 11, 11, 69, 39, 9958]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 175; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms-cff/source.js b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms-cff/source.js new file mode 100644 index 00000000..d86c115c --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms-cff/source.js @@ -0,0 +1,32 @@ +var log = []; +function f(fail) { log.push("f"); if (fail) throw new Error("f"); return 1; } +function g(value) { log.push("g:" + value); } +function emptyCatch(fail) { try { f(fail); } catch (e) {} g(1); } +function blockCatch(fail) { var x = 0; try { f(fail); } catch (e) { x = 1; x = x + 1; g(e.message); } g(x); } +function bindinglessCatch(fail) { try { f(fail); } catch { g(3); } g(4); } +function catchReturn(fail) { try { f(fail); } catch (e) { return 5; } return 6; } +function catchRethrow(fail) { try { f(fail); } catch (e) { g("re"); throw e; } return 7; } +function ifInCatch(fail) { var x = 0; try { f(fail); } catch (e) { if (e.message === "f") { x = 1; } else { x = 2; } } g(x); } +function nestedInCatch(fail) { try { f(fail); } catch (e) { try { f(fail); } catch (e2) { g("inner"); } } g(9); } +function nestedInBody(fail) { try { try { f(fail); } catch (e) { g("inner"); } f(fail); } catch (e) { g("outer"); } g(11); } +function finallyFall(fail) { try { f(fail); } finally { g("fin"); } g(2); } +function finallyReturnCall(fail) { try { return f(fail); } finally { g("fin"); } } +function catchFinallyFall(fail) { var x = 0; try { f(fail); } catch (e) { x = 1; } finally { x = x + 10; } g(x); } +var flag = 1; +function deadCatchCaptured(fail) { try { flag; } catch (e) { flag = 41; } g(flag); } +function deadCatchLocal(fail) { var y = 2; try { y; } catch (e) { y = 42; } g(y); } +function deadThrowPad(fail) { var z = 3; try { z; } finally { g("fin"); } g(z); } +function callFreeTry(fail) { var missing = null; try { if (fail) { definitely_missing_global; } } catch (error) { missing = [error.name, error instanceof ReferenceError]; } return missing; } +function loopFinally(fail) { var n = 0; for (var i = 0; i < 4; i++) { try { if (i === 1) continue; if (i === 3) break; f(fail && i === 2); n += i; } finally { n += 10; } } return n; } +function loopCatchBreak(fail) { var n = 0; for (var i = 0; i < 4; i++) { try { if (i === 3) break; f(fail && i === 1); n += i; } catch (e) { n += 100; } } return n; } +function loopCatchEndsInBreak(fail) { var n = 0; for (var i = 0; i < 4; i++) { try { f(fail && i === 2); n += i; } catch (e) { n += 100; break; } } return n; } +function doTryLoop(fail) { var n = 0, i = 0; do { try { f(fail && i === 1); n += i; } catch (e) { n += 100; } i++; } while (i < 3); return n; } +var forms = [emptyCatch, blockCatch, bindinglessCatch, catchReturn, catchRethrow, ifInCatch, nestedInCatch, nestedInBody, finallyFall, finallyReturnCall, catchFinallyFall, callFreeTry, deadCatchCaptured, deadCatchLocal, deadThrowPad, loopFinally, loopCatchBreak, loopCatchEndsInBreak, doTryLoop]; +window.TEST_OUTPUT = []; +for (var step = 0; step < forms.length * 2; step++) { + var index = step >> 1, fail = (step & 1) === 1; + log = []; + var outcome; + try { outcome = ["value", forms[index](fail)]; } catch (error) { outcome = ["thrown", error.message]; } + window.TEST_OUTPUT.push([index, fail, outcome, log]); +} diff --git a/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms/encoded.js b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms/encoded.js new file mode 100644 index 00000000..261297ca --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */1, /* 1 */"window", /* 2 */"TEST_OUTPUT", /* 3 */0, /* 4 */"length", /* 5 */2, /* 6 */undefined, /* 7 */"value", /* 8 */"thrown", /* 9 */"message", /* 10 */"push", /* 11 */"f", /* 12 */"Error", /* 13 */"g:", /* 14 */3, /* 15 */4, /* 16 */5, /* 17 */6, /* 18 */"re", /* 19 */7, /* 20 */"inner", /* 21 */9, /* 22 */"outer", /* 23 */11, /* 24 */"fin", /* 25 */10, /* 26 */41, /* 27 */42, /* 28 */null, /* 29 */"definitely_missing_global", /* 30 */"name", /* 31 */"ReferenceError", /* 32 */100]; +var BYTECODE = [4, 1, 47, 31, 500, 1, 7, 1, 0, 1, 2, 5, 3, 31, 47, 31, 553, 1, 7, 1, 0, 1, 2, 5, 4, 31, 47, 31, 586, 1, 7, 2, 0, 1, 3, 1, 4, 5, 5, 31, 47, 31, 620, 1, 8, 2, 0, 1, 3, 1, 4, 5, 6, 31, 47, 31, 691, 1, 7, 2, 0, 1, 3, 1, 4, 5, 7, 31, 47, 31, 737, 1, 6, 1, 0, 1, 3, 5, 8, 31, 47, 31, 771, 1, 7, 2, 0, 1, 3, 1, 4, 5, 9, 31, 47, 31, 813, 1, 8, 2, 0, 1, 3, 1, 4, 5, 10, 31, 47, 31, 885, 1, 8, 2, 0, 1, 3, 1, 4, 5, 11, 31, 47, 31, 945, 1, 7, 2, 0, 1, 3, 1, 4, 5, 12, 31, 47, 31, 1017, 1, 8, 2, 0, 1, 3, 1, 4, 5, 13, 31, 47, 31, 1072, 1, 8, 2, 0, 1, 3, 1, 4, 5, 14, 31, 47, 31, 1126, 1, 9, 2, 0, 1, 3, 1, 4, 5, 15, 31, 47, 31, 1196, 1, 7, 2, 0, 1, 16, 1, 4, 5, 17, 31, 47, 31, 1231, 1, 7, 1, 0, 1, 4, 5, 18, 31, 47, 31, 1267, 1, 9, 1, 0, 1, 4, 5, 19, 31, 47, 31, 1317, 1, 8, 0, 0, 5, 20, 31, 47, 31, 1371, 1, 11, 1, 0, 1, 3, 5, 21, 31, 47, 31, 1518, 1, 10, 1, 0, 1, 3, 5, 22, 31, 47, 31, 1632, 1, 10, 1, 0, 1, 3, 5, 23, 31, 47, 31, 1734, 1, 10, 1, 0, 1, 3, 5, 24, 31, 48, 31, 0, 5, 2, 31, 0, 31, 0, 0, 5, 16, 31, 48, 31, 19, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 20, 17, 18, 19, 21, 22, 23, 24, 5, 25, 31, 2, 31, 1, 0, 0, 32, 2, 0, 48, 33, 0, 9, 31, 32, 33, 0, 31, 3, 0, 5, 26, 31, 0, 31, 4, 0, 8, 32, 25, 31, 0, 31, 5, 0, 13, 33, 32, 31, 22, 31, 26, 33, 40, 31, 494, 0, 31, 0, 0, 20, 32, 26, 31, 5, 27, 32, 0, 31, 0, 0, 16, 32, 26, 31, 0, 31, 0, 0, 26, 33, 32, 31, 5, 28, 33, 48, 31, 0, 5, 2, 31, 0, 29, 6, 0, 54, 425, 30, 0, 31, 7, 0, 8, 32, 25, 27, 43, 33, 25, 32, 1, 28, 48, 32, 2, 31, 33, 5, 29, 32, 55, 39, 445, 0, 31, 8, 0, 0, 32, 9, 0, 8, 33, 30, 32, 48, 32, 2, 31, 33, 5, 29, 32, 2, 31, 1, 0, 0, 32, 2, 0, 8, 33, 31, 32, 0, 31, 10, 0, 8, 32, 33, 31, 48, 31, 4, 27, 28, 29, 2, 43, 34, 33, 32, 1, 31, 5, 31, 26, 0, 31, 0, 0, 11, 32, 26, 31, 5, 26, 32, 39, 334, 0, 31, 6, 0, 45, 31, 4, 2, 3, 3, 0, 0, 4, 10, 0, 8, 5, 3, 4, 0, 4, 11, 0, 43, 6, 3, 5, 1, 4, 40, 0, 541, 2, 3, 12, 0, 0, 4, 11, 0, 44, 5, 3, 1, 4, 46, 5, 0, 3, 0, 0, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 3, 3, 0, 0, 4, 10, 0, 8, 5, 3, 4, 0, 4, 13, 0, 11, 6, 4, 0, 43, 4, 3, 5, 1, 6, 0, 3, 6, 0, 45, 3, 4, 2, 54, 602, 3, 3, 4, 0, 42, 5, 4, 1, 0, 55, 39, 602, 3, 4, 1, 0, 5, 0, 0, 42, 6, 4, 1, 5, 0, 4, 6, 0, 45, 4, 4, 2, 0, 5, 3, 0, 5, 3, 5, 54, 643, 4, 3, 5, 0, 42, 6, 5, 1, 0, 55, 39, 677, 0, 5, 0, 0, 5, 3, 5, 0, 5, 0, 0, 11, 6, 3, 5, 5, 3, 6, 3, 5, 1, 0, 6, 9, 0, 8, 7, 4, 6, 42, 6, 5, 1, 7, 3, 5, 1, 42, 6, 5, 1, 3, 0, 5, 6, 0, 45, 5, 4, 2, 54, 707, 3, 3, 4, 0, 42, 5, 4, 1, 0, 55, 39, 719, 3, 4, 1, 0, 5, 14, 0, 42, 6, 4, 1, 5, 3, 4, 1, 0, 5, 15, 0, 42, 6, 4, 1, 5, 0, 4, 6, 0, 45, 4, 4, 2, 54, 753, 3, 3, 4, 0, 42, 5, 4, 1, 0, 55, 39, 759, 0, 4, 16, 0, 45, 4, 0, 4, 17, 0, 45, 4, 0, 4, 6, 0, 45, 4, 4, 2, 54, 787, 3, 3, 4, 0, 42, 5, 4, 1, 0, 55, 39, 801, 3, 4, 1, 0, 5, 18, 0, 42, 6, 4, 1, 5, 46, 3, 0, 4, 19, 0, 45, 4, 0, 4, 6, 0, 45, 4, 4, 2, 0, 5, 3, 0, 5, 3, 5, 54, 836, 4, 3, 5, 0, 42, 6, 5, 1, 0, 55, 39, 871, 0, 5, 9, 0, 8, 6, 4, 5, 0, 5, 11, 0, 26, 7, 6, 5, 40, 7, 864, 0, 5, 0, 0, 5, 3, 5, 39, 871, 0, 5, 5, 0, 5, 3, 5, 3, 5, 1, 42, 6, 5, 1, 3, 0, 5, 6, 0, 45, 5, 4, 2, 54, 901, 3, 3, 5, 0, 42, 6, 5, 1, 0, 55, 39, 927, 54, 915, 4, 3, 5, 0, 42, 6, 5, 1, 0, 55, 39, 927, 3, 5, 1, 0, 6, 20, 0, 42, 7, 5, 1, 6, 3, 5, 1, 0, 6, 21, 0, 42, 7, 5, 1, 6, 0, 5, 6, 0, 45, 5, 4, 2, 54, 987, 3, 54, 964, 3, 3, 4, 0, 42, 5, 4, 1, 0, 55, 39, 976, 3, 4, 1, 0, 5, 20, 0, 42, 6, 4, 1, 5, 3, 4, 0, 42, 5, 4, 1, 0, 55, 39, 999, 3, 4, 1, 0, 5, 22, 0, 42, 6, 4, 1, 5, 3, 4, 1, 0, 5, 23, 0, 42, 6, 4, 1, 5, 0, 4, 6, 0, 45, 4, 4, 2, 59, 1038, 3, 4, 1052, 3, 5, 0, 42, 6, 5, 1, 0, 55, 1, 3, 1054, 39, 1038, 3, 5, 1, 0, 6, 24, 0, 42, 7, 5, 1, 6, 58, 3, 46, 4, 3, 5, 1, 0, 6, 5, 0, 42, 7, 5, 1, 6, 0, 5, 6, 0, 45, 5, 4, 2, 59, 1102, 3, 4, 1116, 3, 5, 0, 42, 6, 5, 1, 0, 5, 4, 6, 1, 3, 1118, 55, 39, 1102, 55, 1, 3, 1120, 39, 1102, 3, 5, 1, 0, 6, 24, 0, 42, 7, 5, 1, 6, 58, 3, 46, 4, 45, 4, 0, 5, 6, 0, 45, 5, 4, 2, 0, 7, 3, 0, 5, 3, 7, 59, 1167, 5, 6, 1180, 54, 1154, 4, 3, 7, 0, 42, 8, 7, 1, 0, 55, 39, 1161, 0, 7, 0, 0, 5, 3, 7, 55, 1, 5, 1182, 39, 1167, 0, 7, 25, 0, 11, 8, 3, 7, 5, 3, 8, 58, 5, 46, 6, 3, 7, 1, 42, 8, 7, 1, 3, 0, 7, 6, 0, 45, 7, 4, 2, 54, 1207, 3, 3, 4, 0, 55, 39, 1214, 0, 4, 26, 0, 7, 0, 4, 3, 4, 1, 3, 5, 0, 42, 6, 4, 1, 5, 0, 4, 6, 0, 45, 4, 4, 2, 0, 5, 5, 0, 5, 3, 5, 54, 1246, 4, 55, 39, 1253, 0, 5, 27, 0, 5, 3, 5, 3, 5, 0, 42, 6, 5, 1, 3, 0, 5, 6, 0, 45, 5, 4, 2, 0, 6, 14, 0, 5, 3, 6, 59, 1287, 4, 5, 1301, 55, 1, 4, 1303, 39, 1287, 3, 6, 0, 0, 7, 24, 0, 42, 8, 6, 1, 7, 58, 4, 46, 5, 3, 6, 0, 42, 7, 6, 1, 3, 0, 6, 6, 0, 45, 6, 4, 2, 0, 5, 28, 0, 5, 3, 5, 54, 1339, 4, 40, 0, 1336, 2, 5, 29, 0, 55, 39, 1363, 0, 5, 30, 0, 8, 6, 4, 5, 2, 5, 31, 0, 31, 7, 4, 5, 48, 5, 2, 6, 7, 5, 3, 5, 45, 3, 0, 5, 6, 0, 45, 5, 4, 2, 0, 7, 3, 0, 5, 3, 7, 0, 7, 3, 0, 5, 4, 7, 0, 7, 15, 0, 22, 8, 4, 7, 40, 8, 1510, 59, 1475, 5, 6, 1488, 0, 7, 0, 0, 26, 8, 4, 7, 40, 8, 1420, 1, 5, 1490, 55, 39, 1475, 0, 7, 14, 0, 26, 8, 4, 7, 40, 8, 1437, 1, 5, 1492, 55, 39, 1475, 3, 7, 0, 5, 8, 0, 40, 8, 1457, 0, 9, 5, 0, 26, 10, 4, 9, 5, 8, 10, 42, 9, 7, 1, 8, 11, 7, 3, 4, 5, 3, 7, 55, 1, 5, 1494, 39, 1475, 0, 7, 25, 0, 11, 8, 3, 7, 5, 3, 8, 58, 5, 46, 6, 39, 1494, 39, 1510, 5, 7, 4, 0, 7, 0, 0, 11, 8, 4, 7, 5, 4, 8, 39, 1387, 45, 3, 0, 7, 6, 0, 45, 7, 4, 2, 0, 6, 3, 0, 5, 3, 6, 0, 6, 3, 0, 5, 4, 6, 0, 6, 15, 0, 22, 7, 4, 6, 40, 7, 1624, 54, 1597, 5, 0, 6, 14, 0, 26, 7, 4, 6, 40, 7, 1562, 55, 39, 1624, 3, 6, 0, 5, 7, 0, 40, 7, 1582, 0, 8, 0, 0, 26, 9, 4, 8, 5, 7, 9, 42, 8, 6, 1, 7, 11, 6, 3, 4, 5, 3, 6, 55, 39, 1608, 0, 6, 32, 0, 11, 7, 3, 6, 5, 3, 7, 5, 6, 4, 0, 6, 0, 0, 11, 7, 4, 6, 5, 4, 7, 39, 1534, 45, 3, 0, 6, 6, 0, 45, 6, 4, 2, 0, 6, 3, 0, 5, 3, 6, 0, 6, 3, 0, 5, 4, 6, 0, 6, 15, 0, 22, 7, 4, 6, 40, 7, 1726, 54, 1697, 5, 3, 6, 0, 5, 7, 0, 40, 7, 1682, 0, 8, 5, 0, 26, 9, 4, 8, 5, 7, 9, 42, 8, 6, 1, 7, 11, 6, 3, 4, 5, 3, 6, 55, 39, 1710, 0, 6, 32, 0, 11, 7, 3, 6, 5, 3, 7, 39, 1726, 5, 6, 4, 0, 6, 0, 0, 11, 7, 4, 6, 5, 4, 7, 39, 1648, 45, 3, 0, 6, 6, 0, 45, 6, 4, 2, 0, 6, 3, 0, 5, 3, 6, 0, 6, 3, 0, 5, 4, 6, 54, 1788, 5, 3, 6, 0, 5, 7, 0, 40, 7, 1773, 0, 8, 0, 0, 26, 9, 4, 8, 5, 7, 9, 42, 8, 6, 1, 7, 11, 6, 3, 4, 5, 3, 6, 55, 39, 1799, 0, 6, 32, 0, 11, 7, 3, 6, 5, 3, 7, 5, 6, 4, 0, 6, 0, 0, 11, 7, 4, 6, 5, 4, 7, 0, 6, 14, 0, 22, 7, 4, 6, 40, 7, 1826, 39, 1750, 45, 3, 0, 6, 6, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 35; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms/source.js b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms/source.js new file mode 100644 index 00000000..d86c115c --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/forms/source.js @@ -0,0 +1,32 @@ +var log = []; +function f(fail) { log.push("f"); if (fail) throw new Error("f"); return 1; } +function g(value) { log.push("g:" + value); } +function emptyCatch(fail) { try { f(fail); } catch (e) {} g(1); } +function blockCatch(fail) { var x = 0; try { f(fail); } catch (e) { x = 1; x = x + 1; g(e.message); } g(x); } +function bindinglessCatch(fail) { try { f(fail); } catch { g(3); } g(4); } +function catchReturn(fail) { try { f(fail); } catch (e) { return 5; } return 6; } +function catchRethrow(fail) { try { f(fail); } catch (e) { g("re"); throw e; } return 7; } +function ifInCatch(fail) { var x = 0; try { f(fail); } catch (e) { if (e.message === "f") { x = 1; } else { x = 2; } } g(x); } +function nestedInCatch(fail) { try { f(fail); } catch (e) { try { f(fail); } catch (e2) { g("inner"); } } g(9); } +function nestedInBody(fail) { try { try { f(fail); } catch (e) { g("inner"); } f(fail); } catch (e) { g("outer"); } g(11); } +function finallyFall(fail) { try { f(fail); } finally { g("fin"); } g(2); } +function finallyReturnCall(fail) { try { return f(fail); } finally { g("fin"); } } +function catchFinallyFall(fail) { var x = 0; try { f(fail); } catch (e) { x = 1; } finally { x = x + 10; } g(x); } +var flag = 1; +function deadCatchCaptured(fail) { try { flag; } catch (e) { flag = 41; } g(flag); } +function deadCatchLocal(fail) { var y = 2; try { y; } catch (e) { y = 42; } g(y); } +function deadThrowPad(fail) { var z = 3; try { z; } finally { g("fin"); } g(z); } +function callFreeTry(fail) { var missing = null; try { if (fail) { definitely_missing_global; } } catch (error) { missing = [error.name, error instanceof ReferenceError]; } return missing; } +function loopFinally(fail) { var n = 0; for (var i = 0; i < 4; i++) { try { if (i === 1) continue; if (i === 3) break; f(fail && i === 2); n += i; } finally { n += 10; } } return n; } +function loopCatchBreak(fail) { var n = 0; for (var i = 0; i < 4; i++) { try { if (i === 3) break; f(fail && i === 1); n += i; } catch (e) { n += 100; } } return n; } +function loopCatchEndsInBreak(fail) { var n = 0; for (var i = 0; i < 4; i++) { try { f(fail && i === 2); n += i; } catch (e) { n += 100; break; } } return n; } +function doTryLoop(fail) { var n = 0, i = 0; do { try { f(fail && i === 1); n += i; } catch (e) { n += 100; } i++; } while (i < 3); return n; } +var forms = [emptyCatch, blockCatch, bindinglessCatch, catchReturn, catchRethrow, ifInCatch, nestedInCatch, nestedInBody, finallyFall, finallyReturnCall, catchFinallyFall, callFreeTry, deadCatchCaptured, deadCatchLocal, deadThrowPad, loopFinally, loopCatchBreak, loopCatchEndsInBreak, doTryLoop]; +window.TEST_OUTPUT = []; +for (var step = 0; step < forms.length * 2; step++) { + var index = step >> 1, fail = (step & 1) === 1; + log = []; + var outcome; + try { outcome = ["value", forms[index](fail)]; } catch (error) { outcome = ["thrown", error.message]; } + window.TEST_OUTPUT.push([index, fail, outcome, log]); +} diff --git a/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/generation.json b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/generation.json new file mode 100644 index 00000000..3dfaffd4 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/generation.json @@ -0,0 +1,21 @@ +{ + "note": "Each cell's encoded.js is the VM encoder's output for its source.js, with the leading disassembly comment block removed (the committed corpus format). Default options draw no randomness; a cell with options names them and the seed of the PRNG that replaced Math.random for the encode (mulberry32: state += 0x6d2b79f5, then the standard mulberry32 output). Regenerating with the same encoder revision, options and seed reproduces the bytes.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5", + "options": {} + }, + "cells": { + "routes": "exceptions raised in the try body, the catch and the finalizer, each against pending returns and throws, with a captured counter", + "forms": "one function per try form (empty, multi-block and binding-less catch; return and rethrow in a catch; conditional and nested try in a catch; try nested in a try body; finally that falls through; return of a call through finally; catch and finally that fall through; call-free try body; a catch and a finalizer whose try body cannot throw; inside loops: continue and break across a finally, break across a catch, a catch ending in break, a do-while whose body starts with the try), each run without and with an exception", + "forms-cff": "the forms source with control flow flattening: a try nested in a catch ends at the jump step its pops and its catch share, not at the outer try's end behind it" + }, + "cellOptions": { + "forms-cff": { + "options": { + "controlFlowFlattening": true + }, + "seed": 1 + } + } +} diff --git a/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/routes/encoded.js b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/routes/encoded.js new file mode 100644 index 00000000..ae3356d7 --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/routes/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"branch", /* 1 */false, /* 2 */true, /* 3 */"bodyThrows", /* 4 */"catchGetterThrows", /* 5 */"catchCallThrows", /* 6 */"finalizerThrows", /* 7 */"window", /* 8 */"TEST_OUTPUT", /* 9 */"map", /* 10 */undefined, /* 11 */10, /* 12 */"method", /* 13 */"body", /* 14 */"handler", /* 15 */null, /* 16 */"name", /* 17 */":", /* 18 */"message", /* 19 */"push", /* 20 */"finally", /* 21 */"RangeError", /* 22 */"finalizer", /* 23 */"join", /* 24 */"right", /* 25 */"left", /* 26 */7, /* 27 */1, /* 28 */100, /* 29 */"body:get", /* 30 */"SyntaxError", /* 31 */0, /* 32 */"catch:get", /* 33 */"TypeError", /* 34 */"catch getter", /* 35 */"catch", /* 36 */"Error", /* 37 */"catch call", /* 38 */200]; +var BYTECODE = [4, 1, 47, 5, 261, 1, 13, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 0, 5, 0, 0, 0, 6, 1, 0, 49, 7, 1, 5, 6, 0, 5, 0, 0, 0, 6, 2, 0, 49, 8, 1, 5, 6, 0, 5, 0, 0, 0, 6, 1, 0, 0, 9, 3, 0, 0, 10, 2, 0, 49, 11, 2, 5, 6, 9, 10, 0, 5, 0, 0, 0, 6, 2, 0, 0, 9, 3, 0, 0, 10, 2, 0, 49, 12, 2, 5, 6, 9, 10, 0, 5, 3, 0, 0, 6, 2, 0, 0, 9, 4, 0, 0, 10, 2, 0, 49, 13, 2, 5, 6, 9, 10, 0, 5, 0, 0, 0, 6, 2, 0, 0, 9, 3, 0, 0, 10, 2, 0, 0, 14, 5, 0, 0, 15, 2, 0, 49, 16, 3, 5, 6, 9, 10, 14, 15, 0, 5, 0, 0, 0, 6, 2, 0, 0, 9, 6, 0, 0, 10, 2, 0, 49, 14, 2, 5, 6, 9, 10, 0, 5, 3, 0, 0, 6, 2, 0, 0, 9, 5, 0, 0, 10, 2, 0, 0, 15, 6, 0, 0, 17, 2, 0, 49, 18, 3, 5, 6, 9, 10, 15, 17, 48, 5, 8, 7, 8, 11, 12, 13, 16, 14, 18, 5, 4, 5, 2, 5, 7, 0, 0, 6, 8, 0, 0, 7, 9, 0, 8, 8, 4, 7, 47, 7, 955, 1, 6, 2, 0, 1, 2, 1, 3, 43, 9, 4, 8, 1, 7, 9, 5, 6, 9, 0, 5, 10, 0, 45, 5, 4, 2, 47, 10, 439, 1, 7, 2, 0, 0, 0, 1, 0, 5, 5, 10, 47, 10, 502, 1, 11, 4, 0, 0, 0, 1, 4, 1, 3, 1, 5, 5, 6, 10, 0, 10, 11, 0, 5, 3, 10, 0, 10, 12, 0, 47, 11, 703, 1, 7, 1, 0, 0, 0, 49, 12, 1, 10, 11, 0, 10, 13, 0, 47, 11, 743, 0, 6, 2, 0, 0, 0, 1, 0, 50, 12, 10, 11, 0, 10, 14, 0, 47, 11, 807, 0, 6, 2, 0, 0, 0, 1, 0, 50, 12, 10, 11, 5, 4, 12, 0, 7, 10, 0, 0, 10, 15, 0, 5, 8, 10, 54, 394, 9, 0, 10, 0, 0, 8, 11, 0, 10, 42, 10, 6, 1, 11, 5, 7, 10, 55, 39, 425, 0, 10, 16, 0, 8, 11, 9, 10, 0, 10, 17, 0, 11, 12, 11, 10, 0, 10, 18, 0, 8, 11, 9, 10, 11, 10, 12, 11, 5, 8, 10, 48, 10, 3, 7, 8, 3, 45, 10, 0, 10, 10, 0, 45, 10, 4, 2, 3, 3, 0, 0, 4, 19, 0, 8, 5, 3, 4, 0, 4, 20, 0, 48, 6, 2, 4, 0, 43, 4, 3, 5, 1, 6, 3, 3, 1, 0, 4, 6, 0, 8, 5, 3, 4, 40, 5, 496, 2, 3, 21, 0, 0, 4, 22, 0, 44, 5, 3, 1, 4, 46, 5, 0, 3, 10, 0, 45, 3, 4, 2, 3, 6, 0, 0, 7, 19, 0, 8, 8, 6, 7, 0, 7, 23, 0, 40, 0, 531, 0, 9, 24, 0, 5, 10, 9, 39, 538, 0, 9, 25, 0, 5, 10, 9, 48, 9, 2, 7, 10, 43, 7, 6, 8, 1, 9, 59, 678, 4, 5, 691, 54, 624, 3, 3, 6, 1, 0, 7, 13, 0, 8, 8, 6, 7, 3, 6, 1, 0, 7, 12, 0, 8, 8, 6, 7, 0, 7, 26, 0, 43, 9, 6, 8, 1, 7, 3, 6, 2, 0, 7, 27, 0, 11, 8, 6, 7, 7, 2, 8, 0, 6, 11, 0, 12, 7, 8, 6, 55, 5, 5, 7, 1, 4, 693, 55, 39, 678, 55, 39, 672, 3, 6, 1, 0, 7, 14, 0, 8, 8, 6, 7, 43, 7, 6, 8, 1, 3, 3, 6, 2, 0, 7, 27, 0, 11, 8, 6, 7, 7, 2, 8, 0, 6, 11, 0, 12, 7, 8, 6, 5, 5, 7, 1, 4, 695, 55, 39, 678, 55, 1, 4, 697, 39, 678, 3, 6, 3, 3, 7, 2, 42, 8, 6, 1, 7, 58, 4, 46, 5, 45, 5, 45, 5, 0, 6, 10, 0, 45, 6, 4, 2, 3, 3, 0, 0, 4, 19, 0, 8, 5, 3, 4, 0, 4, 12, 0, 48, 6, 2, 4, 0, 43, 4, 3, 5, 1, 6, 0, 3, 28, 0, 45, 3, 0, 3, 10, 0, 45, 3, 4, 1, 3, 2, 0, 0, 3, 19, 0, 8, 4, 2, 3, 0, 3, 29, 0, 43, 5, 2, 4, 1, 3, 3, 2, 1, 0, 3, 3, 0, 8, 4, 2, 3, 40, 4, 795, 2, 2, 30, 0, 0, 3, 13, 0, 44, 4, 2, 1, 3, 46, 4, 0, 2, 31, 0, 45, 2, 0, 2, 10, 0, 45, 2, 4, 1, 3, 2, 0, 0, 3, 19, 0, 8, 4, 2, 3, 0, 3, 32, 0, 43, 5, 2, 4, 1, 3, 3, 2, 1, 0, 3, 4, 0, 8, 4, 2, 3, 40, 4, 859, 2, 2, 33, 0, 0, 3, 34, 0, 44, 4, 2, 1, 3, 46, 4, 47, 2, 878, 1, 8, 2, 0, 0, 0, 0, 1, 45, 2, 0, 2, 10, 0, 45, 2, 4, 2, 3, 3, 0, 0, 4, 19, 0, 8, 5, 3, 4, 0, 4, 35, 0, 0, 6, 16, 0, 8, 7, 0, 6, 48, 6, 2, 4, 7, 43, 4, 3, 5, 1, 6, 3, 3, 1, 0, 4, 5, 0, 8, 5, 3, 4, 40, 5, 943, 2, 3, 36, 0, 0, 4, 37, 0, 44, 5, 3, 1, 4, 46, 5, 0, 3, 38, 0, 45, 3, 0, 3, 10, 0, 45, 3, 4, 2, 48, 4, 0, 7, 0, 4, 3, 4, 1, 42, 5, 4, 1, 0, 5, 3, 5, 3, 4, 0, 48, 5, 2, 3, 4, 45, 5, 0, 4, 10, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 19; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/routes/source.js b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/routes/source.js new file mode 100644 index 00000000..2009e64b --- /dev/null +++ b/test/vm/switch/fixtures/vm-switch-structured-emitter/try-region/routes/source.js @@ -0,0 +1,39 @@ +var trace = []; +function scenario(options) { + var count = 10; + var host = { + get body() { trace.push("body:get"); if (options.bodyThrows) throw new SyntaxError("body"); return 0; }, + method: function (value) { trace.push(["method", value]); return 100; }, + get handler() { + trace.push("catch:get"); + if (options.catchGetterThrows) throw new TypeError("catch getter"); + return function (error) { trace.push(["catch", error.name]); if (options.catchCallThrows) throw new Error("catch call"); return 200; }; + } + }; + function finalize(value) { trace.push(["finally", value]); if (options.finalizerThrows) throw new RangeError("finalizer"); } + function region(branch) { + trace.push(["join", branch ? "right" : "left"]); + try { + host.body; + host.method(7); + return ++count - 10; + } catch (error) { + host.handler(error); + return ++count - 10; + } finally { + finalize(count); + } + } + var result, thrown = null; + try { result = region(options.branch); } catch (error) { thrown = error.name + ":" + error.message; } + return [result, thrown, count]; +} +var cases = [ + { branch: false }, { branch: true }, + { branch: false, bodyThrows: true }, { branch: true, bodyThrows: true }, + { bodyThrows: true, catchGetterThrows: true }, + { branch: true, bodyThrows: true, catchCallThrows: true }, + { branch: true, finalizerThrows: true }, + { bodyThrows: true, catchCallThrows: true, finalizerThrows: true } +]; +window.TEST_OUTPUT = cases.map(function (item) { trace = []; var outcome = scenario(item); return [outcome, trace]; }); diff --git a/test/vm/switch/vm-switch-statements.test.js b/test/vm/switch/vm-switch-statements.test.js new file mode 100644 index 00000000..cbf4b9d4 --- /dev/null +++ b/test/vm/switch/vm-switch-statements.test.js @@ -0,0 +1,477 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { diagnoseStandaloneInput } from '../../../src/vm/jsconfuser-vm/diagnose-standalone.js' +import { numericToVmSwitch } from '../../../src/vm/jsconfuser-vm/numeric-to-vm-switch.js' +import normalizeVmInput from '../../../src/visitor/jsconfuser-vm/normalize-source.js' +import { solveControl } from '../../../src/vm/switch/vm-switch-control.js' +import { planStatements } from '../../../src/vm/switch/vm-switch-statements.js' +import { emitVmSwitchProgram } from '../../../src/vm/switch/vm-switch-to-source.js' + +// The regions the backend plans for one function of a lowered model, from the control solver's +// result as emitVmSwitchProgram passes it. +function planFunction(model, functionRecord) { + const cases = model.instructionsByFunction.get(functionRecord.id) + return planStatements( + functionRecord, + cases, + solveControl(functionRecord, cases), + ) +} + +// The numeric frontend supplies the model; these tests read, per function, its cases, the +// control solver's result and the regions the shared backend plans from them. +function planned(relativePath) { + const input = diagnoseStandaloneInput( + normalizeVmInput( + fs.readFileSync( + new URL(`../jsconfuser-vm/fixtures/${relativePath}`, import.meta.url), + 'utf8', + ), + ), + ) + expect(input.ok, JSON.stringify(input.diagnostic)).toBe(true) + const model = numericToVmSwitch(input.model) + return model.functions.functions.map((functionRecord) => { + const cases = model.instructionsByFunction.get(functionRecord.id) + const solved = solveControl(functionRecord, cases) + return { + cases, + solved, + regions: planStatements(functionRecord, cases, solved), + } + }) +} + +const pcOf = (id) => Number(id.slice(id.indexOf('@') + 1)) + +test('plans one linear region for straight-line control', () => { + const [{ regions, cases }] = planned('reference/encoded.js') + expect(regions).toHaveLength(1) + expect(regions[0].kind).toBe('linear') + expect(regions[0].blockIds).toHaveLength(cases.length) +}) + +test('plans a conditional with its merge and condition', () => { + const plans = planned('corpus/raw/f-branching/encoded.js') + const { regions, solved, cases } = plans.find(({ regions }) => + regions.some(({ kind }) => kind === 'conditional'), + ) + const conditional = regions.find(({ kind }) => kind === 'conditional') + expect(conditional.proof).toContain('first block both arms reach') + const outgoing = solved.edges.filter( + ({ sourcePc }) => sourcePc === pcOf(conditional.testBlockId), + ) + expect(outgoing).toContainEqual( + expect.objectContaining({ + kind: 'conditional', + targetPc: conditional.branchTargetPc, + condition: conditional.condition, + }), + ) + expect(outgoing).toContainEqual( + expect.objectContaining({ + kind: 'fallthrough', + targetPc: conditional.fallthroughTargetPc, + }), + ) + expect(conditional.mergeBlockId).toMatch(/^\d+:b@\d+$/) + expect(cases.some(({ pc }) => pc === pcOf(conditional.mergeBlockId))).toBe( + true, + ) +}) + +test('plans natural loops only with a validated header, back-edge, and exit', () => { + const plans = planned('corpus/raw/f-loop-forms/encoded.js') + let count = 0 + for (const { regions, solved } of plans) + for (const loop of regions.filter(({ kind }) => kind === 'natural-loop')) { + count += 1 + expect(loop.proof).toContain('header dominates') + expect(loop.bodyBlockIds).toContain(loop.headerBlockId) + const body = new Set(loop.bodyBlockIds.map(pcOf)) + const header = pcOf(loop.headerBlockId) + const fromBody = solved.edges.filter(({ sourcePc }) => body.has(sourcePc)) + expect(fromBody.some(({ targetPc }) => targetPc === header)).toBe(true) + expect( + fromBody.some( + ({ targetPc }) => targetPc === null || !body.has(targetPc), + ), + ).toBe(true) + } + expect(count).toBeGreaterThan(0) +}) + +// One function per way a statement can end inside a function (fixtures/vm-switch-statements/exits). +// Functions 1-8 follow the source's declaration order; the planned rows pin the decisions and the +// output pins the readable form: one labeled loop per source loop, a guard without `else`. +const exitsDirectory = new URL( + './fixtures/vm-switch-statements/exits/forms/', + import.meta.url, +) +const exitsLoops = { 1: 1, 2: 1, 3: 2, 4: 1, 5: 1, 6: 1, 7: 0, 8: 0 } + +function functionOutput(output, functionId) { + const start = output.indexOf(`function __recovered_function_${functionId}(`) + expect(start).toBeGreaterThanOrEqual(0) + const next = output.indexOf('\n function __recovered_function_', start + 1) + return output.slice(start, next === -1 ? output.length : next) +} + +function observe(code) { + const window = {} + vm.runInNewContext( + code, + { window, performance: { now: () => 0 } }, + { timeout: 5000 }, + ) + return JSON.stringify(window.TEST_OUTPUT) +} + +test('recovers every way a statement ends inside a function in its source form', () => { + const source = fs.readFileSync(new URL('source.js', exitsDirectory), 'utf8') + const diagnosis = diagnoseStandalone( + fs.readFileSync(new URL('encoded.js', exitsDirectory), 'utf8'), + ) + expect(diagnosis.ok, JSON.stringify(diagnosis.diagnostic)).toBe(true) + const { output, statements, functionCount } = diagnosis.result + expect(statements).toHaveLength(functionCount) + expect(output).not.toContain('__recovered_dispatch') + expect(observe(output)).toBe(observe(source)) + for (const [id, loops] of Object.entries(exitsLoops)) + expect( + functionOutput(output, id).match(/: while \(true\) \{/g)?.length ?? 0, + `function ${id}`, + ).toBe(loops) + expect(functionOutput(output, 7)).not.toContain('else') + expect(functionOutput(output, 8)).toContain('} else {') +}) + +// The planner is called directly on the frontend's model, so these rows are read even while +// emission declines. +test('plans a continuation for every loop and merges a guard at its branch target', () => { + const preflight = diagnoseStandaloneInput( + normalizeVmInput( + fs.readFileSync(new URL('encoded.js', exitsDirectory), 'utf8'), + ), + ) + expect(preflight.ok, JSON.stringify(preflight.diagnostic)).toBe(true) + const model = numericToVmSwitch(preflight.model) + const planned = new Map( + model.functions.functions.map((functionRecord) => [ + functionRecord.id, + planFunction(model, functionRecord), + ]), + ) + const loops = [...planned.values()] + .flat() + .filter(({ kind }) => kind === 'natural-loop') + // Seven in the forms and two in the script's driver. + expect(loops).toHaveLength(9) + for (const loop of loops) + expect(loop.continuationBlockId, loop.headerBlockId).not.toBeNull() + const guard = planned.get(7).find(({ kind }) => kind === 'conditional') + expect(guard.mergeBlockId).toBe(`7:b@${guard.branchTargetPc}`) +}) + +// One function per loop whose test is a compound condition (fixtures/vm-switch-statements/ +// loop-tests): the short-circuit branches inside the test close before the branch that leaves the +// loop, so each loop still has its own end and one labeled loop per source loop. +const loopTestsDirectory = new URL( + './fixtures/vm-switch-statements/loop-tests/forms/', + import.meta.url, +) + +test('closes loops whose test is a compound condition', () => { + const encoded = fs.readFileSync( + new URL('encoded.js', loopTestsDirectory), + 'utf8', + ) + const preflight = diagnoseStandaloneInput(normalizeVmInput(encoded)) + expect(preflight.ok, JSON.stringify(preflight.diagnostic)).toBe(true) + const model = numericToVmSwitch(preflight.model) + const loops = model.functions.functions + .flatMap((functionRecord) => planFunction(model, functionRecord)) + .filter(({ kind }) => kind === 'natural-loop') + expect(loops).toHaveLength(7) + for (const loop of loops) + expect(loop.continuationBlockId, loop.headerBlockId).not.toBeNull() + + const diagnosis = diagnoseStandalone(encoded) + expect(diagnosis.ok, JSON.stringify(diagnosis.diagnostic)).toBe(true) + const { output } = diagnosis.result + expect(output).not.toContain('__recovered_dispatch') + const source = fs.readFileSync( + new URL('source.js', loopTestsDirectory), + 'utf8', + ) + expect(observe(output)).toBe(observe(source)) + for (const [id, count] of Object.entries({ + 1: 1, + 2: 1, + 3: 1, + 4: 1, + 5: 1, + 6: 2, + })) + expect( + functionOutput(output, id).match(/: while \(true\) \{/g)?.length ?? 0, + `function ${id}`, + ).toBe(count) +}) + +// One function per way a try statement ends (fixtures/vm-switch-statements/try-ends); functions +// 2-7 follow the source's order. A try ends where its normal exit tail leads, or nowhere when its +// block never completes normally: then the catch keeps the code only it reaches. A continue routed +// through a finally ends at the loop's update (function 7): the try ends at the code after it, and +// the loop is written as a for whose update clause the native continue runs; an update that +// branches (function 8) is one clause too, its conditional written as a conditional expression. +const tryEndsDirectory = new URL( + './fixtures/vm-switch-statements/try-ends/forms/', + import.meta.url, +) + +test('plans where every try statement ends and recovers it in its source form', () => { + const encoded = fs.readFileSync( + new URL('encoded.js', tryEndsDirectory), + 'utf8', + ) + const preflight = diagnoseStandaloneInput(normalizeVmInput(encoded)) + expect(preflight.ok, JSON.stringify(preflight.diagnostic)).toBe(true) + const model = numericToVmSwitch(preflight.model) + const plans = new Map( + model.functions.functions.map((functionRecord) => [ + functionRecord.id, + planFunction(model, functionRecord), + ]), + ) + const tries = new Map( + [...plans].map(([id, regions]) => [ + id, + regions.filter(({ kind }) => kind === 'try'), + ]), + ) + const loopOf = (id) => + plans.get(id).find(({ kind }) => kind === 'natural-loop') + const ends = { + 2: true, + 3: false, + 4: true, + 5: false, + 6: true, + 7: true, + 8: true, + } + for (const [id, ending] of Object.entries(ends)) { + expect(tries.get(Number(id)), `function ${id}`).toHaveLength(1) + expect( + tries.get(Number(id))[0].continuationBlockId !== null, + `function ${id}`, + ).toBe(ending) + } + // The break in function 6 leaves the loop; only function 7's continue enters the update. + expect(loopOf(6).continueBlockId).toBeNull() + const loop = loopOf(7) + const [routed] = tries.get(7) + expect(loop.bodyBlockIds).toContain(loop.continueBlockId) + expect(loop.updateBlockIds[0]).toBe(loop.continueBlockId) + expect(routed.continuationBlockId).not.toBe(loop.continueBlockId) + for (const id of loop.updateBlockIds) + expect(routed.memberBlockIds).not.toContain(id) + // Function 8's continue enters its update before the update's own branch. + const branching = loopOf(8) + const [branchingTry] = tries.get(8) + expect(branching.updateBlockIds[0]).toBe(branching.continueBlockId) + expect( + plans + .get(8) + .some( + ({ kind, testBlockId }) => + kind === 'conditional' && + branching.updateBlockIds.includes(testBlockId), + ), + ).toBe(true) + for (const id of branching.updateBlockIds) + expect(branchingTry.memberBlockIds).not.toContain(id) + + const diagnosis = diagnoseStandalone(encoded) + expect(diagnosis.ok, JSON.stringify(diagnosis.diagnostic)).toBe(true) + const { output } = diagnosis.result + expect(output).not.toContain('__recovered_dispatch') + const source = fs.readFileSync(new URL('source.js', tryEndsDirectory), 'utf8') + expect(observe(output)).toBe(observe(source)) + for (const id of Object.keys(ends)) { + const text = functionOutput(output, id) + expect(text.match(/\btry \{/g), `function ${id}`).toHaveLength(1) + expect( + text.match(/: while \(true\) \{/g)?.length ?? 0, + `function ${id}`, + ).toBe(id === '6' ? 1 : 0) + expect(text.match(/: for \(; ; /g)?.length ?? 0, `function ${id}`).toBe( + id === '7' || id === '8' ? 1 : 0, + ) + } + // Function 3's block always throws: no reachable end, but the frontend's layout end (the dead + // TRY_END; JUMP the compiler places before the catch) closes the catch where the source does. + const [alwaysThrows] = tries.get(3) + expect(alwaysThrows.continuationBlockId).toBeNull() + expect(alwaysThrows.layoutEndBlockId).toMatch(/^3:b@\d+$/) + expect(alwaysThrows.memberBlockIds).not.toContain( + alwaysThrows.layoutEndBlockId, + ) + const throwsText = functionOutput(output, 3) + expect( + throwsText.slice( + throwsText.indexOf('} catch ('), + throwsText.indexOf('"after"'), + ), + ).toMatch(/\n {4}\}\n/) + // Without the hint the catch keeps the code only it reaches, with the same behaviour. + for (const cases of model.instructionsByFunction.values()) + for (const { operation } of cases) delete operation.layoutEnd + const unhinted = emitVmSwitchProgram(model).output + expect(observe(unhinted)).toBe(observe(source)) + const nestedText = functionOutput(unhinted, 3) + expect( + nestedText.slice( + nestedText.indexOf('} catch ('), + nestedText.indexOf('"after"'), + ), + ).not.toMatch(/\n {4}\}\n/) + for (const id of [7, 8]) { + const routedText = functionOutput(output, id) + expect( + routedText.slice( + routedText.indexOf('try {'), + routedText.indexOf('} finally {'), + ), + `function ${id}`, + ).toMatch(/\bcontinue __recovered_loop_/) + } + expect(functionOutput(output, 8)).toMatch(/: for \(; ; [^\n]* \? \(/) + + // A do-while's continue enters its test, so the test is the continue target and the loop reads + // `do { … } while (test)`, whose native continue runs the test. + const doWhile = fs.readFileSync( + new URL('../dowhile-continue/encoded.js', tryEndsDirectory), + 'utf8', + ) + const doWhileModel = numericToVmSwitch( + diagnoseStandaloneInput(normalizeVmInput(doWhile)).model, + ) + const doWhileFunction = doWhileModel.functions.functions.find( + ({ id }) => id === 2, + ) + const doWhileRegions = planFunction(doWhileModel, doWhileFunction) + const doWhileLoop = doWhileRegions.find(({ kind }) => kind === 'natural-loop') + const [doWhileTry] = doWhileRegions.filter(({ kind }) => kind === 'try') + expect(doWhileLoop.updateBlockIds).toBeNull() + expect(doWhileLoop.testBlockIds[0]).toBe(doWhileLoop.continueBlockId) + expect(doWhileTry.continuationBlockId).not.toBeNull() + for (const id of doWhileLoop.testBlockIds) + expect(doWhileTry.memberBlockIds).not.toContain(id) + const recovered = diagnoseStandalone(doWhile) + expect(recovered.ok, JSON.stringify(recovered.diagnostic)).toBe(true) + expect(observe(recovered.result.output)).toBe( + observe( + fs.readFileSync( + new URL('../dowhile-continue/source.js', tryEndsDirectory), + 'utf8', + ), + ), + ) + const doWhileText = functionOutput(recovered.result.output, 2) + expect(doWhileText).toMatch(/: do \{/) + expect(doWhileText).toMatch(/\} while \(\(/) + expect( + doWhileText.slice( + doWhileText.indexOf('try {'), + doWhileText.indexOf('} finally {'), + ), + ).toMatch(/\bcontinue __recovered_loop_/) + + // Control flow flattening shuffles the blocks, with each catch placed elsewhere. The try still + // ends where its body and its catch rejoin, and every function reads as the plain cell does. + const flattened = fs.readFileSync( + new URL('../forms-cff/encoded.js', tryEndsDirectory), + 'utf8', + ) + const flattenedModel = numericToVmSwitch( + diagnoseStandaloneInput(normalizeVmInput(flattened)).model, + ) + for (const [id, ending] of Object.entries(ends)) { + const functionRecord = flattenedModel.functions.functions.find( + (item) => item.id === Number(id), + ) + const flattenedTries = planFunction(flattenedModel, functionRecord).filter( + ({ kind }) => kind === 'try', + ) + expect(flattenedTries, `function ${id}`).toHaveLength(1) + expect( + flattenedTries[0].continuationBlockId !== null, + `function ${id}`, + ).toBe(ending) + } + const flattenedDiagnosis = diagnoseStandalone(flattened) + expect( + flattenedDiagnosis.ok, + JSON.stringify(flattenedDiagnosis.diagnostic), + ).toBe(true) + const flattenedOutput = flattenedDiagnosis.result.output + expect(flattenedOutput).not.toContain('__recovered_dispatch') + expect(observe(flattenedOutput)).toBe(observe(source)) + for (const id of Object.keys(ends)) { + const text = functionOutput(flattenedOutput, id) + const plain = functionOutput(output, id) + expect(text.match(/\btry \{/g), `function ${id}`).toHaveLength(1) + for (const loop of [/: while \(true\) \{/g, /: for \(; ; /g]) + expect(text.match(loop)?.length ?? 0, `function ${id}`).toBe( + plain.match(loop)?.length ?? 0, + ) + } + + // A break out of a switch across a catch that completes normally: the try has two exit tails, + // the break's into the switch's end and its normal one. The catch's own completion reaches the + // normal end first, in the compiler's layout and in a flattened one that places the switch's + // end first; the switch holds the try with its catch, and the break stays inside the try. + const switchBreakSource = fs.readFileSync( + new URL('../switch-break/source.js', tryEndsDirectory), + 'utf8', + ) + for (const cell of ['switch-break', 'switch-break-cff']) { + const encodedCell = fs.readFileSync( + new URL(`../${cell}/encoded.js`, tryEndsDirectory), + 'utf8', + ) + const cellModel = numericToVmSwitch( + diagnoseStandaloneInput(normalizeVmInput(encodedCell)).model, + ) + const regions = planFunction( + cellModel, + cellModel.functions.functions.find(({ id }) => id === 2), + ) + const [switchRow] = regions.filter(({ kind }) => kind === 'switch') + const [tryRow] = regions.filter(({ kind }) => kind === 'try') + expect(switchRow, cell).toBeDefined() + expect(switchRow.memberBlockIds, cell).toContain(tryRow.setupBlockId) + expect(tryRow.continuationBlockId, cell).not.toBeNull() + expect(tryRow.continuationBlockId, cell).not.toBe( + switchRow.continuationBlockId, + ) + const cellDiagnosis = diagnoseStandalone(encodedCell) + expect(cellDiagnosis.ok, JSON.stringify(cellDiagnosis.diagnostic)).toBe( + true, + ) + const cellOutput = cellDiagnosis.result.output + expect(cellOutput, cell).not.toContain('__recovered_dispatch') + expect(observe(cellOutput), cell).toBe(observe(switchBreakSource)) + const text = functionOutput(cellOutput, 2) + expect(text.match(/: switch \(/g), cell).toHaveLength(1) + expect(text.match(/\btry \{/g), cell).toHaveLength(1) + expect( + text.slice(text.indexOf('try {'), text.indexOf('} catch (')), + cell, + ).toMatch(/\bbreak __recovered_switch_/) + } +}) From c59065021f3bbe65e44d52e922002ada24d2e350 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 11/12] feat(decode-js): add the jsconfuser-vm and sequential targets Exposes the VM decoder through the CLI: jsconfuser-vm decodes a standalone VM program, and the sequential target runs the outer js-confuser decoder first for layered input. The review renderer writes an encoded program's dispatch rendering for inspection. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- README.md | 20 +- package.json | 1 + scripts/render-vm-switch.mjs | 55 + src/main.js | 65 + src/plugin/jsconfuser-vm-sequential.js | 69 + src/plugin/jsconfuser-vm.js | 28 + test/scripts/render-vm-switch.test.js | 72 + .../exception-region-parser.test.js | 214 +++ .../jsconfuser-vm/extract-container.test.js | 443 +++++ .../concealed-constants-exceptions/encoded.js | 1019 ++++++++++ .../qualification.json | 69 + .../concealed-constants-exceptions/source.js | 1 + .../exceptionRoutes--baseline--172.js | 1019 ++++++++++ .../exceptionRoutes--baseline--173.js | 1019 ++++++++++ .../exceptionRoutes--baseline--174.js | 1019 ++++++++++ .../exceptionRoutes--classObfuscation--172.js | 952 ++++++++++ .../exceptionRoutes--classObfuscation--173.js | 966 ++++++++++ .../exceptionRoutes--classObfuscation--174.js | 946 ++++++++++ .../exceptionRoutes--minify--172.js | 18 + .../exceptionRoutes--minify--173.js | 18 + .../exceptionRoutes--minify--174.js | 18 + ...tionRoutes--minifyClassObfuscation--172.js | 18 + ...tionRoutes--minifyClassObfuscation--173.js | 18 + ...tionRoutes--minifyClassObfuscation--174.js | 20 + .../fixtures/exception-region/expected.json | 1 + .../fixtures/exception-region/provenance.json | 406 ++++ .../fixtures/exception-region/source.js | 1 + .../layer-pair-arithmetic/generation.json | 78 + .../layer-pair-arithmetic/outer-complete.js | 1 + .../outer-without-string-concealing.js | 1 + .../fixtures/layer-pair-arithmetic/source.js | 5 + .../fixtures/layer-pair-arithmetic/vm-only.js | 1035 ++++++++++ .../baseline-172.encoded.js | 1019 ++++++++++ .../baseline-173.encoded.js | 1019 ++++++++++ .../baseline-174.encoded.js | 1019 ++++++++++ .../classObfuscation-172.encoded.js | 952 ++++++++++ .../classObfuscation-173.encoded.js | 966 ++++++++++ .../classObfuscation-174.encoded.js | 946 ++++++++++ .../exception-routes.expected.json | 19 + .../exception-routes.source.js | 1 + .../minify-172.encoded.js | 18 + .../minify-173.encoded.js | 18 + .../minify-174.encoded.js | 18 + .../minifyClassObfuscation-172.encoded.js | 18 + .../minifyClassObfuscation-173.encoded.js | 18 + .../minifyClassObfuscation-174.encoded.js | 20 + .../native-qualification.json | 562 ++++++ .../minified-class-admission/provenance.md | 20 + .../minified-class-boot/encoded-169.js | 16 + .../minified-class-boot/encoded-170.js | 17 + .../minified-class-boot/encoded-171.js | 16 + .../qualification-169.json | 61 + .../qualification-170.json | 61 + .../qualification-171.json | 61 + .../fixtures/minified-class-boot/source.js | 1 + .../arithmetic--baseline--172.js | 1019 ++++++++++ .../arithmetic--baseline--173.js | 1019 ++++++++++ .../arithmetic--baseline--174.js | 1019 ++++++++++ .../arithmetic--classObfuscation--172.js | 952 ++++++++++ .../arithmetic--classObfuscation--173.js | 966 ++++++++++ .../arithmetic--classObfuscation--174.js | 946 ++++++++++ .../arithmetic--minify--172.js | 15 + .../arithmetic--minify--173.js | 15 + .../arithmetic--minify--174.js | 15 + ...arithmetic--minifyClassObfuscation--172.js | 16 + ...arithmetic--minifyClassObfuscation--173.js | 16 + ...arithmetic--minifyClassObfuscation--174.js | 17 + .../arithmetic.source.js | 1 + ...tionRoutes--minifyClassObfuscation--172.js | 18 + ...tionRoutes--minifyClassObfuscation--173.js | 18 + ...tionRoutes--minifyClassObfuscation--174.js | 20 + .../exceptionRoutes.source.js | 1 + .../minified-class-frontend/provenance.json | 414 ++++ .../randomized-class/arithmetic-181.js | 960 ++++++++++ .../randomized-class/arithmetic-182.js | 952 ++++++++++ .../randomized-class/arithmetic-183.js | 958 ++++++++++ .../arithmetic-minified-191.js | 18 + .../arithmetic-minified-192.js | 19 + .../arithmetic-minified-193.js | 19 + .../randomized-class/arithmetic.baseline.js | 1019 ++++++++++ .../randomized-class/arithmetic.source.js | 1 + .../randomized-class/exceptionRoutes-181.js | 960 ++++++++++ .../randomized-class/exceptionRoutes-182.js | 952 ++++++++++ .../randomized-class/exceptionRoutes-183.js | 958 ++++++++++ .../exceptionRoutes-minified-191.js | 20 + .../exceptionRoutes-minified-192.js | 21 + .../exceptionRoutes-minified-193.js | 21 + .../exceptionRoutes.baseline.js | 1019 ++++++++++ .../exceptionRoutes.source.js | 1 + .../fixtures/randomized-class/provenance.json | 1672 +++++++++++++++++ .../fixtures/randomized-class/spread-181.js | 960 ++++++++++ .../fixtures/randomized-class/spread-182.js | 952 ++++++++++ .../fixtures/randomized-class/spread-183.js | 958 ++++++++++ .../randomized-class/spread-minified-191.js | 18 + .../randomized-class/spread-minified-192.js | 19 + .../randomized-class/spread-minified-193.js | 19 + .../randomized-class/spread.baseline.js | 1019 ++++++++++ .../randomized-class/spread.source.js | 1 + .../randomized-runtime-exceptions/encoded.js | 1021 ++++++++++ .../qualification.json | 69 + .../randomized-runtime-exceptions/source.js | 1 + test/vm/jsconfuser-vm/integration/cli.test.js | 295 +++ .../integration/debugger-policy.test.js | 82 + .../jsconfuser-vm/integration/harness.test.js | 203 ++ .../integration/integration.test.js | 62 + .../jsconfuser-vm/integration/plugin.test.js | 137 ++ .../integration/sequential.test.js | 219 +++ .../minified-class-admission.behavior.test.js | 39 + .../minified-class-admission.test.js | 136 ++ .../minified-class-behavior-support.js | 59 + .../minified-class-frontend.test.js | 51 + .../vm/jsconfuser-vm/randomized-class.test.js | 161 ++ .../randomized-minified-class.test.js | 133 ++ .../runtime-binding-identities.test.js | 73 + test/vm/jsconfuser-vm/try-region.test.js | 52 + 115 files changed, 39276 insertions(+), 1 deletion(-) create mode 100644 scripts/render-vm-switch.mjs create mode 100644 src/plugin/jsconfuser-vm-sequential.js create mode 100644 src/plugin/jsconfuser-vm.js create mode 100644 test/scripts/render-vm-switch.test.js create mode 100644 test/vm/jsconfuser-vm/exception-region-parser.test.js create mode 100644 test/vm/jsconfuser-vm/extract-container.test.js create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--172.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--173.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--174.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--172.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--173.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--174.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--172.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--173.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--174.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--172.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--173.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--174.js create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/expected.json create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/provenance.json create mode 100644 test/vm/jsconfuser-vm/fixtures/exception-region/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/generation.json create mode 100644 test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/outer-complete.js create mode 100644 test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/outer-without-string-concealing.js create mode 100644 test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/vm-only.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-172.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-173.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-174.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-172.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-173.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-174.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/exception-routes.expected.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/exception-routes.source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-172.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-173.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-174.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-172.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-173.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-174.encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/native-qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-admission/provenance.md create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-169.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-170.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-171.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-169.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-170.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-171.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-boot/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--172.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--173.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--174.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--172.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--173.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--174.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--172.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--173.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--174.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--172.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--173.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--174.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic.source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--172.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--173.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--174.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes.source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-class-frontend/provenance.json create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-181.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-182.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-183.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-191.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-192.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-193.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic.baseline.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic.source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-181.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-182.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-183.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-191.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-192.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-193.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes.baseline.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes.source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/provenance.json create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/spread-181.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/spread-182.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/spread-183.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-191.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-192.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-193.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/spread.baseline.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-class/spread.source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/integration/cli.test.js create mode 100644 test/vm/jsconfuser-vm/integration/debugger-policy.test.js create mode 100644 test/vm/jsconfuser-vm/integration/harness.test.js create mode 100644 test/vm/jsconfuser-vm/integration/integration.test.js create mode 100644 test/vm/jsconfuser-vm/integration/plugin.test.js create mode 100644 test/vm/jsconfuser-vm/integration/sequential.test.js create mode 100644 test/vm/jsconfuser-vm/minified-class-admission.behavior.test.js create mode 100644 test/vm/jsconfuser-vm/minified-class-admission.test.js create mode 100644 test/vm/jsconfuser-vm/minified-class-behavior-support.js create mode 100644 test/vm/jsconfuser-vm/minified-class-frontend.test.js create mode 100644 test/vm/jsconfuser-vm/randomized-class.test.js create mode 100644 test/vm/jsconfuser-vm/randomized-minified-class.test.js create mode 100644 test/vm/jsconfuser-vm/runtime-binding-identities.test.js create mode 100644 test/vm/jsconfuser-vm/try-region.test.js diff --git a/README.md b/README.md index 2ff49c37..2411ba75 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,15 @@ readable source. One target per run, selected with `-t`: +The result-aware `jsconfuser-vm` target is listed in the dedicated section below. + | `-t` | target | |---|---| | `common` | frequently-seen local obfuscation, not tied to any one tool — unreachable code, nested blocks, constant expressions, raw strings | | `jjencode` | jjencode, in the variant emitted by sojson.com | | `sojson` | sojson | | `sojsonv7` | sojson v7 | +| `jsconfuser-vm` | JS-Confuser numeric VM containers via the standalone result-aware adapter | | `obfuscator` | [javascript-obfuscator](https://github.com/javascript-obfuscator/javascript-obfuscator) (obfuscator.io) | | `obfuscatorx` | the same obfuscator, version-aware — see below for how it differs | | `jsconfuser` | [JS-Confuser](https://github.com/MichaelXF/js-confuser) | @@ -50,6 +53,15 @@ Covers JS-Confuser 2.x up to and including the `high` preset. Which transforms a reversed, which are not, and why are documented in the [decode-nexus jsconfuser plugin reference](https://github.com/echo094/decode-nexus/blob/main/skills/decode-js/plugins/jsconfuser.md). +### `jsconfuser-vm` + +Decodes accepted numeric VM containers without executing target code. Each attempt writes the +adapter's JavaScript output and a JSON result record containing `status`, `diagnostic`, and the +standalone schema/proof metadata. The result path defaults to `.result.json`; pass +`--result path.json` to choose an explicit path. A declined input is written byte-for-byte to the +output, recorded with `status: "declined"`, and exits successfully. The `--result` option is +target-specific; legacy targets keep their string/null output contract and do not write sidecars. + ## Usage **If problems occur during installation and execution, please check the requirements of @@ -72,7 +84,7 @@ npm run decode -- -t type [-i input.js] [-o output.js] [-v] ``` `xxx` is one of the predefined commands, each a shorthand for one target — `deob`, -`dejsc`, `deso`, `desov7`. See the `scripts` field in [package.json](package.json). +`dejsc`, `dejsc-vm`, `deso`, `desov7`. See the `scripts` field in [package.json](package.json). The default input file is `input.js`. The file cannot contain additional codes other than obfuscated code (such as non-obfuscated code). @@ -81,6 +93,12 @@ The default output file is `output.js`. `-v` turns on per-pass progress tracing, which is off by default. +For the result-aware VM target, use `--result` when an explicit result path is preferred: + +```shell +npm run decode -- -t jsconfuser-vm -i encoded.js -o decoded.js --result decoded.json +``` + ## Related Projects * [cilame/v_jstools](https://github.com/cilame/v_jstools) diff --git a/package.json b/package.json index 779a911e..63f190dc 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "deob": "node src/main.js -t obfuscator", "deobx": "node src/main.js -t obfuscatorx", "dejsc": "node src/main.js -t jsconfuser", + "dejsc-vm": "node src/main.js -t jsconfuser-vm", "deso": "node src/main.js -t sojson", "desov7": "node src/main.js -t sojsonv7", "test": "vitest --config vitest.config.js", diff --git a/scripts/render-vm-switch.mjs b/scripts/render-vm-switch.mjs new file mode 100644 index 00000000..bd738c22 --- /dev/null +++ b/scripts/render-vm-switch.mjs @@ -0,0 +1,55 @@ +import fs from 'node:fs' +import path from 'node:path' +import { parseArgs } from 'node:util' +import { diagnoseStandaloneInput } from '../src/vm/jsconfuser-vm/diagnose-standalone.js' +import { numericToVmSwitch } from '../src/vm/jsconfuser-vm/numeric-to-vm-switch.js' +import { emitVmSwitchProgram } from '../src/vm/switch/vm-switch-to-source.js' + +const usage = `Usage: + node scripts/render-vm-switch.mjs --source encoded.js --output-dir directory + +Writes switch-case.js: the step-one decoded program, without a demo host shim or output logger; +execute it under the same host conditions as the encoded input. The output directory may exist, +but the tool refuses to overwrite an existing artifact. The tool renders code; it does not execute +the encoded input or generated output.` + +function main() { + const { values } = parseArgs({ + options: { + source: { type: 'string' }, + 'output-dir': { type: 'string' }, + help: { type: 'boolean', default: false }, + }, + }) + if (values.help) { + process.stdout.write(`${usage}\n`) + return + } + if (!values.source || !values['output-dir']) throw new Error(usage) + + const diagnosis = diagnoseStandaloneInput( + fs.readFileSync(values.source, 'utf8'), + ) + if (!diagnosis.ok) + throw new Error( + `Numeric VM diagnosis declined: ${diagnosis.diagnostic.code}: ${diagnosis.diagnostic.message}`, + ) + const model = numericToVmSwitch(diagnosis.model) + const output = `${emitVmSwitchProgram(model, { dispatch: true }).output}\n` + const directory = path.resolve(values['output-dir']) + const jsPath = path.join(directory, 'switch-case.js') + if (fs.existsSync(jsPath)) + throw new Error( + 'Review output already exists; choose a fresh output directory', + ) + fs.mkdirSync(directory, { recursive: true }) + fs.writeFileSync(jsPath, output, { flag: 'wx' }) + process.stdout.write(`${jsPath}\n`) +} + +try { + main() +} catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : error}\n`) + process.exitCode = 1 +} diff --git a/src/main.js b/src/main.js index 59501be5..b68d1bc5 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,5 @@ import fs from 'fs' +import path from 'node:path' import { parseArgs } from 'node:util' import PluginCommon from './plugin/common.js' import PluginJjencode from './plugin/jjencode.js' @@ -8,6 +9,8 @@ import PluginSojsonV7 from './plugin/sojsonv7.js' import PluginObfuscator from './plugin/obfuscator.js' import PluginObfuscatorX from './plugin/obfuscatorx.js' import PluginAwsc from './plugin/awsc.js' +import PluginJsconfuserVm from './plugin/jsconfuser-vm.js' +import PluginJsconfuserVmSequential from './plugin/jsconfuser-vm-sequential.js' import logger from './utility/logger.js' // Read arguments @@ -16,12 +19,14 @@ const { values } = parseArgs({ type: { type: 'string', short: 't', default: 'common' }, input: { type: 'string', short: 'i', default: 'input.js' }, output: { type: 'string', short: 'o', default: 'output.js' }, + result: { type: 'string' }, verbose: { type: 'boolean', short: 'v', default: false }, }, }) const type = values.type const encodeFile = values.input const decodeFile = values.output +const resultFile = values.result // Per-pass progress tracing, off unless asked for. `DECODE_JS_DEBUG=1` does the same for a // caller that imports a plugin directly rather than going through this entry point. if (values.verbose) { @@ -40,6 +45,8 @@ const plugins = { obfuscator: PluginObfuscator, obfuscatorx: PluginObfuscatorX, awsc: PluginAwsc, + 'jsconfuser-vm': PluginJsconfuserVm, + 'jsconfuser-vm-sequential': PluginJsconfuserVmSequential, } const main = () => { @@ -52,6 +59,18 @@ const main = () => { return } + const resultAwareTypes = new Set([ + 'jsconfuser-vm', + 'jsconfuser-vm-sequential', + ]) + if (resultFile && !resultAwareTypes.has(type)) { + console.error( + 'The --result option is only supported for jsconfuser-vm and jsconfuser-vm-sequential', + ) + process.exitCode = 1 + return + } + // Read the source code let sourceCode try { @@ -62,6 +81,52 @@ const main = () => { return } + if (resultAwareTypes.has(type)) { + let record + try { + record = plugins[type](sourceCode) + } catch (e) { + console.error(`Cannot decode input ${encodeFile}: ${e.message}`) + process.exitCode = 1 + return + } + + const targetResultFile = resultFile || `${decodeFile}.result.json` + if (path.resolve(targetResultFile) === path.resolve(decodeFile)) { + console.error('Output and result files must be different') + process.exitCode = 1 + return + } + console.log(`Status: ${record.status}`) + if (record.diagnostic) { + console.error( + `Diagnostic: ${record.diagnostic.code}: ${record.diagnostic.message}`, + ) + } + + try { + fs.writeFileSync(decodeFile, record.output) + } catch (e) { + console.error(`Cannot write output file ${decodeFile}: ${e.message}`) + process.exitCode = 1 + return + } + + try { + fs.writeFileSync(targetResultFile, `${JSON.stringify(record, null, 2)}\n`) + } catch (e) { + console.error( + `Cannot write result file ${targetResultFile}: ${e.message}`, + ) + process.exitCode = 1 + return + } + + console.log(`Output written: ${decodeFile}`) + console.log(`Result written: ${targetResultFile}`) + return + } + // Purify the source code const code = plugins[type](sourceCode) diff --git a/src/plugin/jsconfuser-vm-sequential.js b/src/plugin/jsconfuser-vm-sequential.js new file mode 100644 index 00000000..a353bc1d --- /dev/null +++ b/src/plugin/jsconfuser-vm-sequential.js @@ -0,0 +1,69 @@ +import PluginJsconfuser from './jsconfuser.js' +import PluginJsconfuserVm from './jsconfuser-vm.js' + +const COORDINATOR_SCHEMA = 'jsconfuser-vm-sequential.v1' + +function deepFreeze(value) { + if (!value || typeof value !== 'object' || Object.isFrozen(value)) + return value + Object.freeze(value) + for (const child of Object.values(value)) deepFreeze(child) + return value +} + +function declined(input, stages, failedStage, code, message) { + return deepFreeze({ + schemaVersion: COORDINATOR_SCHEMA, + ok: false, + status: 'declined', + input, + output: input, + result: null, + diagnostic: { stage: failedStage, code, message }, + failedStage, + stages, + }) +} + +export function decodeJsconfuserVmSequential(source) { + const outerSource = PluginJsconfuser(source) + if (outerSource === null) + return declined( + source, + { outer: { status: 'declined', ok: false }, vm: { status: 'pending' } }, + 'outer', + 'outer-declined', + 'Outer decoder declined the input', + ) + + const vmRecord = PluginJsconfuserVm(outerSource) + const outer = { status: 'success', ok: true, output: outerSource } + if (!vmRecord.ok) + return declined( + source, + { + outer, + vm: { status: 'declined', ok: false, diagnostic: vmRecord.diagnostic }, + }, + 'vm', + vmRecord.diagnostic.code, + vmRecord.diagnostic.message, + ) + + return deepFreeze({ + schemaVersion: COORDINATOR_SCHEMA, + ok: true, + status: 'success', + input: source, + output: vmRecord.output, + result: vmRecord.result, + diagnostic: null, + failedStage: null, + stages: { + outer, + vm: { status: 'success', ok: true, output: vmRecord.output }, + }, + }) +} + +export default decodeJsconfuserVmSequential diff --git a/src/plugin/jsconfuser-vm.js b/src/plugin/jsconfuser-vm.js new file mode 100644 index 00000000..304a4359 --- /dev/null +++ b/src/plugin/jsconfuser-vm.js @@ -0,0 +1,28 @@ +import { diagnoseStandalone } from '../vm/jsconfuser-vm/decode-standalone.js' +import normalizeVmInput from '../visitor/jsconfuser-vm/normalize-source.js' + +const ADAPTER_SCHEMA = 'jsconfuser-vm-adapter.v1' + +/** + * Recover js-confuser VM source and retain the standalone diagnosis for the caller. + * + * The adapter deliberately returns one result-aware record rather than projecting the + * standalone result to a string. A caller can write `output` and the accompanying + * `result`/`diagnostic` without diagnosing the source again. Declines are atomic: their + * output is the exact input source, not a partially emitted program. + */ +export function decodeJsconfuserVm(source) { + const diagnosis = diagnoseStandalone(normalizeVmInput(source)) + const success = diagnosis.ok + return Object.freeze({ + schemaVersion: ADAPTER_SCHEMA, + ok: success, + status: success ? 'success' : 'declined', + input: source, + output: success ? diagnosis.result.output : source, + result: success ? diagnosis.result : null, + diagnostic: success ? null : diagnosis.diagnostic, + }) +} + +export default decodeJsconfuserVm diff --git a/test/scripts/render-vm-switch.test.js b/test/scripts/render-vm-switch.test.js new file mode 100644 index 00000000..3ba9007a --- /dev/null +++ b/test/scripts/render-vm-switch.test.js @@ -0,0 +1,72 @@ +import { spawnSync } from 'node:child_process' +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { expect, test } from 'vitest' + +const repository = fileURLToPath(new URL('../../', import.meta.url)) +const script = path.join(repository, 'scripts/render-vm-switch.mjs') +const fixture = fileURLToPath( + new URL( + '../vm/jsconfuser-vm/fixtures/readability/arithmetic/encoded.js', + import.meta.url, + ), +) + +function run(...args) { + return spawnSync(process.execPath, args, { + cwd: repository, + encoding: 'utf8', + }) +} + +function execute(source) { + const context = { window: {}, performance: { now: () => 0 } } + runInNewContext(source, context, { timeout: 5000 }) + return context.window +} + +test('step-one switch output preserves the encoded fixture behavior', () => { + const directory = mkdtempSync( + path.join(os.tmpdir(), 'decode-js-switch-review-'), + ) + try { + const sourceOutput = path.join(directory, 'source') + const sourceRun = run( + script, + '--source', + fixture, + '--output-dir', + sourceOutput, + ) + expect(sourceRun.status, sourceRun.stderr).toBe(0) + + const sourcePath = path.join(sourceOutput, 'switch-case.js') + const code = readFileSync(sourcePath, 'utf8') + expect(existsSync(path.join(sourceOutput, 'switch-model.json'))).toBe(false) + expect(code).toContain('switch (__recovered_pc)') + expect(code).toContain('case 17:') + expect(code).not.toContain('catch (__recovered_error)') + expect(code).not.toContain('__recovered_handlers') + expect(code).not.toContain('console.log') + expect(code).not.toContain('globalThis.window =') + const encodedWindow = execute(readFileSync(fixture, 'utf8')) + const decodedWindow = execute(code) + expect(decodedWindow).toEqual(encodedWindow) + expect(decodedWindow.TEST_OUTPUT).toBe(7) + + const overwrite = run( + script, + '--source', + fixture, + '--output-dir', + sourceOutput, + ) + expect(overwrite.status).not.toBe(0) + expect(overwrite.stderr).toContain('Review output already exists') + } finally { + rmSync(directory, { recursive: true, force: true }) + } +}) diff --git a/test/vm/jsconfuser-vm/exception-region-parser.test.js b/test/vm/jsconfuser-vm/exception-region-parser.test.js new file mode 100644 index 00000000..dc37d1ea --- /dev/null +++ b/test/vm/jsconfuser-vm/exception-region-parser.test.js @@ -0,0 +1,214 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import { expectConsistentState } from '../../helper.js' +import { diagnoseStandaloneInput } from '../../../src/vm/jsconfuser-vm/diagnose-standalone.js' +import { numericToVmSwitch } from '../../../src/vm/jsconfuser-vm/numeric-to-vm-switch.js' +import { decodeJsconfuserVm } from '../../../src/plugin/jsconfuser-vm.js' +import { emitVmSwitchProgram } from '../../../src/vm/switch/vm-switch-to-source.js' + +const directory = new URL('fixtures/exception-region/', import.meta.url) +const provenance = JSON.parse( + fs.readFileSync(new URL('provenance.json', directory), 'utf8'), +) +const read = (file) => fs.readFileSync(new URL(file, directory), 'utf8') +const baseline = () => { + const input = diagnoseStandaloneInput(read(provenance.records[0].encoded)) + expect(input.ok, JSON.stringify(input.diagnostic)).toBe(true) + return structuredClone(numericToVmSwitch(input.model)) +} +const caseAt = (model, pc) => + model.instructionsByFunction.get(2).find((item) => item.pc === pc) +const snapshot = (model) => + JSON.stringify(model, (_, value) => + value instanceof Map + ? [...value] + : value instanceof Set + ? [...value] + : value, + ) +// Each damage breaks one property the try-region rule relies on; the unchanged data emits. +function refusal(change, reason) { + const model = baseline() + expect(emitVmSwitchProgram(structuredClone(model)).output).toContain( + '} finally {', + ) + change(model) + const before = snapshot(model) + expect(() => emitVmSwitchProgram(model)).toThrowError(reason) + expect(snapshot(model)).toBe(before) +} +function members(node, type) { + const found = [] + const walk = (item) => { + if (!item || typeof item !== 'object') return + if (item.type === type) found.push(item) + for (const [key, value] of Object.entries(item)) { + if (['loc', 'start', 'end'].includes(key)) continue + if (Array.isArray(value)) value.forEach(walk) + else if (value?.type) walk(value) + } + } + walk(node) + return found +} +for (const record of provenance.records) { + test(`${record.id}: exact frozen data reaches current public structured output`, () => { + expect(provenance.encoderPin).toBe( + '20c5b96bf57337c568579352758d7685358650de', + ) + expect(record.rng.seed).toBe(record.seed) + expect(record.options.minify).toBe( + ['minify', 'minifyClassObfuscation'].includes(record.profile), + ) + expect(record.options.classObfuscation).toBe( + ['classObfuscation', 'minifyClassObfuscation'].includes(record.profile), + ) + const source = read(record.encoded), + input = diagnoseStandaloneInput(source) + expect(input.ok, JSON.stringify(input.diagnostic)).toBe(true) + const beforeNumeric = snapshot(input.model), + model = numericToVmSwitch(input.model), + before = snapshot(model) + const shared = emitVmSwitchProgram(model), + result = decodeJsconfuserVm(source) + expect(result.ok, JSON.stringify(result.diagnostic)).toBe(true) + expect(result.output).toBe(shared.output) + expect(result.output).not.toBe(source) + expect(Object.isFrozen(result)).toBe(true) + expect(Object.isFrozen(result.result)).toBe(true) + expect(result.result.proof).toMatchObject({ + freshJavaScriptEmission: true, + finalParseValidated: true, + finalResidueValidated: true, + }) + expect(result.result.finalJavaScriptSemantics).toBe(false) + const ast = parse(result.output) + traverse(ast, {}) + expectConsistentState(ast, result.output) + const fn = members(ast, 'FunctionDeclaration').find( + ({ id }) => id.name === '__recovered_function_2', + ) + const outer = fn.body.body.find((item) => item.type === 'TryStatement'), + inner = outer.block.body[0] + expect(outer.finalizer.type).toBe('BlockStatement') + expect(inner.handler.param.name).toBe('__recovered_error') + expect(members(inner.block, 'IfStatement')).toHaveLength(5) // three bytecode tests and two global-read guards + expect(members(inner.block, 'ReturnStatement')).toHaveLength(1) + expect(members(inner.handler, 'ReturnStatement')).toHaveLength(1) + expect(members(inner.block, 'ThrowStatement')).toHaveLength(3) // explicit throw plus two global guards + const writes = members(fn, 'AssignmentExpression').filter( + (node) => + node.left.type === 'MemberExpression' && + node.left.object?.type === 'MemberExpression' && + node.left.object.object?.name === '__recovered_captures', + ) + expect(writes).toHaveLength(2) + for (const phase of [inner.block, inner.handler]) { + const returnBlock = members(phase, 'BlockStatement').find( + (item) => item.body.at(-1)?.type === 'ReturnStatement', + ) + expect(returnBlock).toBeDefined() + const statements = returnBlock.body + const tail = statements.at(-1) + // Saved return is committed after the captured increment and before the finalizer. + expect(tail.type).toBe('ReturnStatement') + expect(tail.argument.name).toBe('__recovered_r_2_5') + expect(statements.at(-2).expression.left.name).toBe('__recovered_r_2_5') + expect(statements.at(-3).expression.left.property.name).toBe('value') + } + expect(members(outer.finalizer, 'ReturnStatement')).toHaveLength(0) + expect( + members(outer.finalizer, 'Identifier').some(({ name }) => + ['__recovered_r_2_4', '__recovered_r_2_5'].includes(name), + ), + ).toBe(false) + expect( + members(ast, 'Identifier').some(({ name }) => + [ + 'VM', + 'BYTECODE', + 'CONSTANTS', + 'Closure', + 'Upvalue', + 'decodeBytecode', + '__recovered_pc', + '__recovered_dispatch', + '__recovered_handlers', + ].includes(name), + ), + ).toBe(false) + expect( + shared.simplification.functions + .find(({ functionId }) => functionId === 2) + .copies.every( + ({ sourceRegister, destinationRegister }) => + ![3, 4, 5].includes(sourceRegister) && + ![3, 4, 5].includes(destinationRegister), + ), + ).toBe(true) + expect(snapshot(model)).toBe(before) + expect(snapshot(input.model)).toBe(beforeNumeric) + }) +} +// The control solver reads the resume's targets from the definitions reaching its register, so a +// damaged continuation write or resume register leaves the resume without a finite target set. +test('frozen finalizer cannot overwrite continuation', () => + refusal((model) => { + caseAt(model, 326).operation.destination = 4 + }, /indirect-jump@351 has no finite target set/)) +test('frozen return pad cannot read the wrong payload', () => + refusal((model) => { + caseAt(model, 355).operation.source = 9 + }, /return payload at 355/)) +test('frozen finalizer cannot read the pending payload', () => + refusal((model) => { + caseAt(model, 337).operation.value = { + kind: 'register', + index: 5, + } + }, /completion register used at 337/)) +test('frozen throw pad must rethrow the pending payload', () => + refusal((model) => { + caseAt(model, 353).operation.source = 9 + }, /has no payload throw pad/)) +test('frozen finalizer resume must read its continuation', () => + refusal((model) => { + caseAt(model, 351).operation.source = 7 + }, /indirect-jump@351 has no finite target set/)) + +for (const name of [ + 'focused/return-through-finally', + 'focused/caught-return-through-finally', + 'focused/protected-conditional-catch', + 'focused/terminal-switch-arms', +]) { + test(`existing ${name} remains source-derived parser supported`, () => { + const source = fs.readFileSync( + new URL(`fixtures/corpus/${name}/encoded.js`, import.meta.url), + 'utf8', + ) + const result = decodeJsconfuserVm(source) + expect(result.ok, JSON.stringify(result.diagnostic)).toBe(true) + expect(result.output).not.toBe(source) + expect(result.output).not.toContain('__recovered_dispatch') + const ast = parse(result.output) + traverse(ast, {}) + expectConsistentState(ast, result.output) + }) +} + +test('a stale numeric finalizer target retains atomic public refusal', () => { + const source = fs.readFileSync( + new URL( + 'fixtures/corpus/negative/finally-stale-target/encoded.js', + import.meta.url, + ), + 'utf8', + ) + const result = decodeJsconfuserVm(source) + expect(result.ok).toBe(false) + expect(result.result).toBeNull() + expect(result.output).toBe(source) +}) diff --git a/test/vm/jsconfuser-vm/extract-container.test.js b/test/vm/jsconfuser-vm/extract-container.test.js new file mode 100644 index 00000000..3b3fd398 --- /dev/null +++ b/test/vm/jsconfuser-vm/extract-container.test.js @@ -0,0 +1,443 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { parse } from '@babel/parser' +import generate from '@babel/generator' +import traverse from '@babel/traverse' +import { expectConsistentState } from '../../helper.js' +import decodeVm from '../../../src/plugin/jsconfuser-vm.js' +import { + diagnoseContainer, + extractContainer, + extractContainerFromSource, +} from '../../../src/vm/jsconfuser-vm/extract-container.js' +const repositoryRoot = new URL('../../../', import.meta.url) +const referencePath = new URL( + 'test/vm/jsconfuser-vm/fixtures/reference/encoded.js', + repositoryRoot, +) +const debugReferencePath = new URL( + 'test/vm/jsconfuser-vm/fixtures/reference/encoded.debug.js', + repositoryRoot, +) +const reference = fs.readFileSync(referencePath, 'utf8') +const debugReference = fs.readFileSync(debugReferencePath, 'utf8') + +function deepFrozen(value) { + if (!value || typeof value !== 'object') return true + return ( + Object.isFrozen(value) && + Object.values(value).every((child) => deepFrozen(child)) + ) +} + +function renameBindings(source) { + const bindingAliases = new Map([ + ['CONSTANTS', 'poolBindingAlias'], + ['BYTECODE', 'wordcodeBindingAlias'], + ['MAIN_START_PC', 'startPcBindingAlias'], + ['MAIN_REG_COUNT', 'regCountBindingAlias'], + ['ENCODE_BYTECODE', 'encodedFlagAlias'], + ['TIMING_CHECKS', 'timingFlagAlias'], + ['HEADER_SIZE', 'headerSizeAlias'], + ['FRAME_START', 'frameStartAlias'], + ['OP', 'opcodeMapAlias'], + ['SENTINELS', 'sentinelMapAlias'], + ['SLOTS', 'slotMapAlias'], + ['Upvalue', 'upvalueConstructorAlias'], + ['Closure', 'closureConstructorAlias'], + ['VM', 'vmConstructorAlias'], + ['decodeBytecode', 'decoderFunctionAlias'], + ['globals', 'globalObjectAlias'], + ['vm', 'vmInstanceAlias'], + ]) + const methodAliases = new Map([ + ['_read', 'readMethodAlias'], + ['_write', 'writeMethodAlias'], + ['_close', 'closeMethodAlias'], + ['_operand', 'operandMethodAlias'], + ['_pushFrame', 'pushFrameMethodAlias'], + ['captureUpvalue', 'captureMethodAlias'], + ['_constant', 'constantMethodAlias'], + ['_closeUpvaluesFor', 'closeUpvaluesMethodAlias'], + ['run', 'runMethodAlias'], + ]) + const ast = parse(source, { + allowReturnOutsideFunction: true, + sourceType: 'script', + }) + traverse(ast, { + Program(path) { + for (const [from, to] of bindingAliases) { + if (path.scope.getBinding(from)) path.scope.rename(from, to) + } + }, + MemberExpression(path) { + if (path.node.computed || path.node.property.type !== 'Identifier') { + return + } + const alias = methodAliases.get(path.node.property.name) + if (alias) path.node.property.name = alias + }, + }) + return generate(ast, { comments: true }).code +} + +function topLevelDeclaration(source, name) { + const ast = parse(source, { + allowReturnOutsideFunction: true, + sourceType: 'script', + }) + return ast.program.body.find( + (statement) => + statement.type === 'VariableDeclaration' && + statement.declarations.some( + (declaration) => declaration.id.name === name, + ), + ) +} + +function withoutTopLevelDeclaration(source, name) { + const ast = parse(source, { + allowReturnOutsideFunction: true, + sourceType: 'script', + }) + ast.program.body = ast.program.body.filter( + (statement) => + !( + statement.type === 'VariableDeclaration' && + statement.declarations.some( + (declaration) => declaration.id.name === name, + ) + ), + ) + return generate(ast, { comments: true }).code +} + +function replaceOnce(source, from, to) { + const index = source.indexOf(from) + expect(index, `fixture text not found: ${from}`).toBeGreaterThanOrEqual(0) + return `${source.slice(0, index)}${to}${source.slice(index + from.length)}` +} + +// Constant simplification can leave `argc === 65535` where the runtime reads the sentinel map; a +// stable value reads the same whichever way it is spelled. +function literalSentinels() { + const source = reference.replace( + /argc === SENTINELS\.CALL_SPREAD/g, + 'argc === 65535', + ) + expect(source.match(/argc === 65535/g)).toHaveLength(3) + return source +} + +test('map reads and literal sentinels yield the same recovered program', () => { + const source = literalSentinels() + const ast = parse(source) + traverse(ast, {}) + const before = JSON.stringify(ast) + const diagnosis = diagnoseContainer(ast) + expect(diagnosis.ok, JSON.stringify(diagnosis.diagnostic)).toBe(true) + expect(diagnosis.result).toEqual(extractContainer(reference)) + expect(JSON.stringify(ast)).toBe(before) + expectConsistentState(ast, source, { sourceType: 'script' }) + const recovered = decodeVm(source) + expect(recovered.ok, JSON.stringify(recovered.diagnostic)).toBe(true) + expect(recovered.output).not.toBe(source) + expect(recovered.output).toBe(decodeVm(reference).output) +}) + +test('extracts the pinned reference payload and the known scalar values', () => { + const result = extractContainerFromSource(reference) + + expect(result).not.toBeNull() + expect(result.schemaVersion).toBe('jsconfuser-vm-container.v1') + expect(result.encoding).toBe('numeric-u32') + expect(result.roles.words.values).toHaveLength(32) + expect(result.roles.op.values).toEqual({ + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59, + }) + expect(result.roles.sentinels.values).toEqual({ CALL_SPREAD: 65535 }) + expect(result.roles.slots.values).toEqual({ + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7, + }) + expect( + Object.fromEntries( + Object.entries(result.roles.scalars).map(([role, { value }]) => [ + role, + value, + ]), + ), + ).toEqual({ + MAIN_START_PC: 0, + MAIN_REG_COUNT: 7, + ENCODE_BYTECODE: false, + TIMING_CHECKS: false, + HEADER_SIZE: 8, + FRAME_START: 1, + }) +}) + +test('renamed bindings and prototype methods leave the located payload unchanged', () => { + expect(extractContainer(renameBindings(reference))).toEqual( + extractContainer(reference), + ) +}) + +test('is independent of debug and disassembly comments', () => { + const result = extractContainerFromSource(debugReference) + expect(result).not.toBeNull() + expect(result.roles.scalars.MAIN_REG_COUNT.value).toBe(7) +}) + +test('accepts an AST without mutating it and returns deeply frozen data', () => { + const ast = parse(reference, { + allowReturnOutsideFunction: true, + sourceType: 'script', + }) + const before = JSON.stringify(ast) + const result = extractContainer(ast) + + expect(JSON.stringify(ast)).toBe(before) + expect(deepFrozen(result)).toBe(true) + expect(() => result.roles.words.values.push(99)).toThrow() + expect(() => { + result.roles.scalars.MAIN_START_PC.value = 1 + }).toThrow() +}) + +test('also accepts a generated baseline with a program-specific register count', () => { + const source = fs.readFileSync( + new URL( + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/encoded.js', + repositoryRoot, + ), + 'utf8', + ) + const result = extractContainerFromSource(source) + + expect(result).not.toBeNull() + expect(result.roles.scalars.MAIN_REG_COUNT.value).toBe(24) +}) + +const negatives = [ + ['ordinary JavaScript near-miss', 'var answer = 42;', 'missing-role'], + [ + 'missing mandatory root scalar', + withoutTopLevelDeclaration(reference, 'MAIN_REG_COUNT'), + 'missing-role', + ], + [ + 'malformed word array', + replaceOnce(reference, 'var BYTECODE = [4,', 'var BYTECODE = [4294967296,'), + 'missing-role', + ], +] + +test.each(negatives)( + 'declines %s before producing a result', + (_label, source, code) => { + const report = diagnoseContainer(source) + + expect(report.ok).toBe(false) + expect(report.result).toBeNull() + expect(report.diagnostic.code).toBe(code) + expect(extractContainer(source)).toBeNull() + }, +) + +test('returns a stable diagnostic rather than throwing for malformed input', () => { + const report = diagnoseContainer('var =') + + expect(report).toMatchObject({ + ok: false, + result: null, + diagnostic: { code: 'extractor-error' }, + }) + expect(report.diagnostic.message).toContain('Unexpected token') +}) + +// Each entry succeeds on a parsed File and reports a frozen decline on the same parse's bare +// Program. +test.each([['reference/encoded.js', diagnoseContainer, 'extractor-error']])( + '%s accepts File and refuses a raw Program', + (name, entry, code) => { + const source = fs.readFileSync( + new URL(`test/vm/jsconfuser-vm/fixtures/${name}`, repositoryRoot), + 'utf8', + ) + const fresh = () => + parse(source, { sourceType: 'script', allowReturnOutsideFunction: true }) + const file = fresh() + const fileBefore = JSON.stringify(file) + const accepted = entry(file) + if (accepted && 'ok' in accepted) expect(accepted.ok).toBe(true) + else expect(accepted?.type).toBe('File') + expect(JSON.stringify(file)).toBe(fileBefore) + + const program = fresh().program + const programBefore = JSON.stringify(program) + if (code) { + const refused = entry(program) + expect(refused).toMatchObject({ + ok: false, + result: null, + diagnostic: { code }, + }) + expect(deepFrozen(refused)).toBe(true) + } else { + expect(() => entry(program)).toThrow( + new TypeError( + "Cannot read properties of undefined (reading 'getBlockParent')", + ), + ) + } + expect(JSON.stringify(program)).toBe(programBefore) + }, +) + +// Moved-declaration spelling: every top-level declaration becomes a bare name +// in one leading var plus a `name = value` statement where it stood, as an +// outer moved-declarations pass emits it. +function movedDeclarations(source) { + const ast = parse(source) + const names = [] + const body = [] + for (const statement of ast.program.body) { + if (statement.type === 'VariableDeclaration' && statement.kind === 'var') { + for (const declarator of statement.declarations) { + names.push(declarator.id.name) + if (declarator.init) + body.push( + `${declarator.id.name} = ${generate(declarator.init).code};`, + ) + } + } else if (statement.type === 'FunctionDeclaration') { + names.push(statement.id.name) + const fn = generate({ ...statement, type: 'FunctionExpression' }).code + body.push(`${statement.id.name} = ${fn};`) + } else body.push(generate(statement).code) + } + return `var ${names.join(', ')};\n${body.join('\n')}\n` +} + +test('roles are read from the single defining write, not the declarator', () => { + const moved = movedDeclarations(reference) + expect(moved).not.toContain('var BYTECODE =') + const ast = parse(moved) + const before = JSON.stringify(ast) + const diagnosis = diagnoseContainer(ast) + expect(diagnosis.ok, JSON.stringify(diagnosis.diagnostic)).toBe(true) + expect(diagnosis.result).toEqual(extractContainer(reference)) + expect(JSON.stringify(ast)).toBe(before) + expect(decodeVm(moved).output).toBe(decodeVm(reference).output) +}) + +// Runtime-layer options re-spell the interpreter and leave the payload unchanged, so +// extractContainerFromSource locates words, pool and root descriptor by the boot skeleton and the +// decode behaves as the source (fixture runtime-layers: classObfuscation and minify, with +// handlerTable and case shuffle, and with aliasedOpcodes or specializedOpcodes, whose decoy +// parameters minify reuses as locals; minify with aliasedOpcodes, whose readers find the handlers +// by behaviour, also with a handler table and with each other added-opcode option; minify with +// randomizeOpcodes, whose opcode values get their meaning from handler behaviour, with and without +// a handler table). +test.each([ + 'class-minify', + 'class-minify-table-shuffle', + 'class-minify-aliased', + 'class-minify-specialized', + 'minify-aliased', + 'minify-randomized', + 'minify-table-randomized', + 'minify-table-aliased', + 'minify-table-macro', + 'minify-table-specialized', + 'minify-table-anti', +])('locates the payload of a %s runtime by its boot skeleton', (name) => { + const directory = new URL( + 'test/vm/jsconfuser-vm/fixtures/runtime-layers/', + repositoryRoot, + ) + const encoded = fs.readFileSync( + new URL(`${name}.encoded.js`, directory), + 'utf8', + ) + const source = fs.readFileSync(new URL('source.js', directory), 'utf8') + const run = (code) => { + const window = {} + vm.runInNewContext(code, { window, performance: { now: () => 0 } }) + return window.TEST_OUTPUT + } + const container = diagnoseContainer(encoded) + expect(container.ok, JSON.stringify(container.diagnostic)).toBe(true) + const decoded = decodeVm(encoded) + expect(decoded.ok, JSON.stringify(decoded.diagnostic)).toBe(true) + expect(run(decoded.output)).toEqual(run(source)) +}) diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/encoded.js new file mode 100644 index 00000000..d289c37f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"O1Lezd1GyNq2Ty06", /* 1 */"eUFzy2C/WCtF51Z5WZFXGVaDWQ9Nkw==", /* 2 */-1825795512, /* 3 */-1100832949, /* 4 */"7LDuGNKB5wnykOwo6qceWw==", /* 5 */undefined, /* 6 */453096803, /* 7 */"/aTJPTysvlQmxA==", /* 8 */"KZyZDCZ0p+UwrKY8TNRQTA==", /* 9 */"cilxkQX5goINB3Czfy8=", /* 10 */true]; +var BYTECODE = [4, 1, 47, 3, 69, 1, 9, 0, 0, 5, 2, 3, 2, 3, 0, 2568121930, 0, 4, 1, 3561773946, 0, 5, 2, 2469171785, 32, 6, 5, 42, 5, 2, 1, 6, 0, 6, 3, 3194134351, 42, 7, 2, 1, 6, 2, 6, 0, 2568121930, 0, 8, 4, 1174738869, 8, 9, 6, 8, 48, 6, 3, 5, 7, 9, 9, 3, 4, 6, 0, 3, 5, 0, 45, 3, 4, 2, 59, 149, 4, 5, 167, 54, 126, 3, 0, 6, 6, 453096803, 22, 7, 0, 6, 40, 7, 105, 2, 6, 7, 1836679046, 0, 7, 8, 1941740078, 44, 8, 6, 1, 7, 46, 8, 0, 6, 2, 2469171785, 11, 7, 0, 6, 55, 5, 5, 7, 1, 4, 169, 55, 39, 149, 55, 39, 143, 0, 6, 9, 1443041042, 8, 7, 3, 6, 5, 5, 7, 1, 4, 171, 55, 39, 149, 55, 1, 4, 173, 39, 149, 2, 6, 0, 2568121930, 0, 7, 4, 1174738869, 0, 8, 10, 0, 9, 6, 7, 8, 58, 4, 46, 5, 45, 5, 45, 5, 0, 6, 5, 0, 45, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/qualification.json new file mode 100644 index 00000000..419c9e64 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/qualification.json @@ -0,0 +1,69 @@ +{ + "id": "exceptions--concealConstants--25", + "case": "exceptions", + "profile": "concealConstants", + "seed": 25, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--172.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--172.js new file mode 100644 index 00000000..e72aa743 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--172.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--173.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--173.js new file mode 100644 index 00000000..e72aa743 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--173.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--174.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--174.js new file mode 100644 index 00000000..e72aa743 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--baseline--174.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--172.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--172.js new file mode 100644 index 00000000..076106ef --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--172.js @@ -0,0 +1,952 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(absSlot, fake_1, regs) { + this.i = fake_1; + this.g = regs; // shared reference to VM._regs flat array + this.e = absSlot; // absolute index; stable as long as frame is alive + this.I = false; + this.P = undefined; +} +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(constants, fake_13, globals, bytecode) { + this.q = bytecode; + this.K = constants; + this.u = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.F = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.W = fake_13; + this.g = []; + this.s = 1; + this.n = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +// Closure & Frame +function Closure(fake_11, fake_9, fake_10, fake_12, fn) { + this.t = fn; + this.Z = []; + this.E = [fake_10, fake_11]; + this.prototype = {}; // <- default prototype object for `new` + this.aa = fake_9 || fake_12; +} +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +VM.prototype.R = function (fp, fake_18, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.g[fp + 7] + slot; + var uvs = this.F || (this.F = []); + this.U = fake_18; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, null, this.g)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.d = function (fake_16, closure, retDst, args, fake_17, thisVal) { + var fn = closure.t; + var regs = this.g; + var fp = this.s; + var size = 8 + fn.l; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 8; + regs[fp + 0] = fn.p; + regs[fp + 1] = this.n; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.T; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.p; + this.D = fake_17 && fake_16; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.s = end; + if (args) { + if (fn.r) { + var restSlot = fn.T - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.l; a++) regs[base + a] = args[a]; + } + if (fn.T < fn.l) regs[base + fn.T] = args; + } + this.n = fp; +}; +Upvalue.prototype.c = function (fake_4, v, fake_5, fake_3) { + this.m = fake_5; + if (this.I) this.P = v;else this.g[this.e] = v; + this.z = fake_3 || fake_4; +}; +VM.prototype.M = function (fake_20, idxIn, keyIn, fake_19) { + var idx = idxIn ?? this.N("k", "z"); + var key = keyIn ?? this.N(undefined, null); + var v = this.K[idx]; + this.Y = fake_20; + this.S = fake_19; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.B = function (fp, fake_21, fake_22) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.F; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this.g; + this.b = fake_21 || fake_22; + var hi = fp + regs[fp + 6]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.a("y", {}, null); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.a = function (fake_6, fake_7, fake_8) { + this.o = fake_6; + this.P = this.g[this.e]; + this.I = true; + this.Q = fake_7; + this.h = fake_8; +}; +Upvalue.prototype.w = function (fake_2) { + this.j = fake_2; + return this.I ? this.P : this.g[this.e]; +}; +VM.prototype.N = function (fake_15, fake_14) { + this.L = fake_15 || fake_14; + return this.q[this.g[this.n + 0]++]; +}; +VM.prototype.O = function (fake_23, fake_25, closure, args, thisVal, fake_24) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.G = fake_24; + this.v = [fake_23, fake_25]; + this.d(null, closure, 0, args, "y", thisVal); + while (true) { + var fp = this.n; + var regs = this.g; + var bc = this.q; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.q[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.q.length; i++) this.q[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.s; i2++) this.g[i2] = undefined; + op = 39; + regs[fp + 0] = this.q.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 0: + { + var dst = this.N(42, 53); + regs[base + dst] = this.M(); + break; + } + case 1: + { + var dst = this.N(null, undefined); + regs[base + dst] = this.N([], undefined); + break; + } + case 2: + { + var dst = this.N([], null); + var globalName = this.M(); + if (!(globalName in this.u)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.u[globalName]; + break; + } + case 3: + { + var dst = this.N({}, 55); + regs[base + dst] = regs[fp + 4].Z[this.N("z", {})].w(undefined); + break; + } + case 4: + { + var dst = this.N(undefined, "x"); + regs[base + dst] = regs[fp + 3]; + break; + } + case 5: + { + var dst = this.N(null, null); + regs[base + dst] = regs[base + this.N({}, 7)]; + break; + } + case 6: + { + // globals[globalName] = regs[src] + this.u[this.M()] = regs[base + this.N(undefined, [])]; + break; + } + case 7: + { + var uvIdx = this.N("z", 95); + regs[fp + 4].Z[uvIdx].c(undefined, regs[base + this.N(50, [])], null, "z"); + break; + } + case 8: + { + // dst = regs[obj][regs[key]] + var dst = this.N({}, []); + var obj = regs[base + this.N("q", undefined)]; + var key = regs[base + this.N(undefined, undefined)]; + regs[base + dst] = obj[key]; + break; + } + case 9: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.N({}, "k")]; + var key = regs[base + this.N(null, 58)]; + var val = regs[base + this.N(null, {})]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 10: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.N([], {}); + var obj = regs[base + this.N(undefined, "q")]; + var key = regs[base + this.N({}, 90)]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 11: + { + var dst = this.N(undefined, "k"); + var a = regs[base + this.N([], null)]; + regs[base + dst] = a + regs[base + this.N([], 53)]; + break; + } + case 12: + { + var dst = this.N([], "y"); + var a = regs[base + this.N({}, {})]; + regs[base + dst] = a - regs[base + this.N(undefined, null)]; + break; + } + case 13: + { + var dst = this.N(undefined, {}); + var a = regs[base + this.N(undefined, undefined)]; + regs[base + dst] = a * regs[base + this.N("z", "k")]; + break; + } + case 14: + { + var dst = this.N("x", 4); + var a = regs[base + this.N({}, "q")]; + regs[base + dst] = a / regs[base + this.N(26, [])]; + break; + } + case 15: + { + var dst = this.N([], null); + var a = regs[base + this.N([], undefined)]; + regs[base + dst] = a % regs[base + this.N({}, "q")]; + break; + } + case 60: + { + // Math.pow instead of `**` + var dst = this.N({}, 75); + var a = regs[base + this.N({}, 82)]; + regs[base + dst] = Math.pow(a, regs[base + this.N({}, null)]); + break; + } + case 16: + { + var dst = this.N("z", []); + var a = regs[base + this.N(null, {})]; + regs[base + dst] = a & regs[base + this.N(41, 80)]; + break; + } + case 17: + { + var dst = this.N({}, undefined); + var a = regs[base + this.N("y", [])]; + regs[base + dst] = a | regs[base + this.N([], {})]; + break; + } + case 18: + { + var dst = this.N(null, 16); + var a = regs[base + this.N("x", {})]; + regs[base + dst] = a ^ regs[base + this.N(null, null)]; + break; + } + case 19: + { + var dst = this.N(47, []); + var a = regs[base + this.N("z", [])]; + regs[base + dst] = a << regs[base + this.N([], [])]; + break; + } + case 20: + { + var dst = this.N({}, 37); + var a = regs[base + this.N(undefined, 53)]; + regs[base + dst] = a >> regs[base + this.N(10, {})]; + break; + } + case 21: + { + var dst = this.N({}, "x"); + var a = regs[base + this.N(undefined, [])]; + regs[base + dst] = a >>> regs[base + this.N(null, [])]; + break; + } + + // Comparison (dst, src1, src2) + case 22: + { + var dst = this.N([], undefined); + var a = regs[base + this.N(undefined, undefined)]; + regs[base + dst] = a < regs[base + this.N(null, 42)]; + break; + } + case 23: + { + var dst = this.N({}, null); + var a = regs[base + this.N("x", undefined)]; + regs[base + dst] = a > regs[base + this.N(null, 16)]; + break; + } + case 24: + { + var dst = this.N(null, 26); + var a = regs[base + this.N("x", 21)]; + regs[base + dst] = a <= regs[base + this.N("z", {})]; + break; + } + case 25: + { + var dst = this.N({}, 34); + var a = regs[base + this.N({}, null)]; + regs[base + dst] = a >= regs[base + this.N(undefined, {})]; + break; + } + case 26: + { + var dst = this.N(91, null); + var a = regs[base + this.N([], {})]; + regs[base + dst] = a === regs[base + this.N(null, {})]; + break; + } + case 27: + { + var dst = this.N(60, "x"); + var a = regs[base + this.N(10, {})]; + regs[base + dst] = a !== regs[base + this.N(3, null)]; + break; + } + case 28: + { + var dst = this.N(undefined, {}); + var a = regs[base + this.N({}, null)]; + regs[base + dst] = a == regs[base + this.N({}, "k")]; + break; + } + case 29: + { + var dst = this.N([], "z"); + var a = regs[base + this.N(52, "y")]; + regs[base + dst] = a != regs[base + this.N("z", 40)]; + break; + } + case 30: + { + var dst = this.N(11, null); + var a = regs[base + this.N({}, null)]; + regs[base + dst] = a in regs[base + this.N("q", null)]; + break; + } + case 31: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.N("x", []); + var obj = regs[base + this.N(null, undefined)]; + regs[base + dst] = obj instanceof regs[base + this.N(1, null)]; + break; + } + + // Unary (dst, src) + case 32: + { + var dst = this.N("y", []); + regs[base + dst] = -regs[base + this.N(76, [])]; + break; + } + case 33: + { + var dst = this.N(undefined, undefined); + regs[base + dst] = +regs[base + this.N(null, {})]; + break; + } + case 34: + { + var dst = this.N([], {}); + regs[base + dst] = !regs[base + this.N(91, 87)]; + break; + } + case 35: + { + var dst = this.N(null, "k"); + regs[base + dst] = ~regs[base + this.N(19, {})]; + break; + } + case 36: + { + var dst = this.N(44, 19); + regs[base + dst] = typeof regs[base + this.N([], {})]; + break; + } + case 37: + { + var dst = this.N("q", "x"); + this.N("k", undefined); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 38: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.N("x", null); + var name = this.M(); + var val = Object.prototype.hasOwnProperty.call(this.u, name) ? this.u[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39: + regs[fp + 0] = this.N({}, 89); + break; + case 40: + { + var src = this.N(undefined, null); + var target = this.N({}, []); + if (!regs[base + src]) regs[fp + 0] = target; + break; + } + case 41: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.N({}, null); + var target = this.N({}, null); + if (regs[base + src]) regs[fp + 0] = target; + break; + } + + // Calls + case 42: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.N("k", undefined); + var callee = regs[base + this.N({}, "z")]; + var argc = this.N([], 30); + var args; + if (argc === 65535) { + args = regs[base + this.N([], [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.N(null, [])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.d("q", closure, dst << 1, args, "q", this.u); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 43: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.N("k", []); + var receiver = regs[base + this.N(null, "y")]; + var callee = regs[base + this.N([], "k")]; + var argc = this.N([], null); + var args; + if (argc === 65535) { + args = regs[base + this.N([], null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.N(31, {})]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.d("z", closure, dst << 1, args, "z", receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 44: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.N([], "k"); + var callee = regs[base + this.N({}, 51)]; + var argc = this.N(63, []); + var args; + if (argc === 65535) { + args = regs[base + this.N(undefined, 78)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.N(69, null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.d(undefined, closure, dst << 1 | 1, args, [], Object.create(closure.prototype || null)); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 45: + { + var retVal = regs[base + this.N(null, "q")]; + this.B(fp, "k", []); // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.s = fp; + this.n = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 46: + throw regs[base + this.N([], null)]; + + // Closures + case 47: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.N("y", {}); + var startPc = this.N({}, null); + var paramCount = this.N(55, "x"); + var regCount = this.N(12, 29); + var uvCount = this.N([], "q"); + var hasRest = this.N(38, undefined); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.N(null, undefined); + var uvIndex = this.N({}, 2); + uvDescs[i] = { + J: isLocalRaw, + y: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + T: paramCount, + l: regCount, + p: startPc, + r: hasRest + }; + var closure = new Closure(51, null, [], 59, fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.J) { + closure.Z.push(this.R(fp, {}, uvd.y)); + } else { + closure.Z.push(regs[fp + 4].Z[uvd.y]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.K, undefined, self.u, self.q).O([], {}, c, Array.prototype.slice.call(arguments), this == null ? self.u : this, 45); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 48: + { + // dst, count, [elemReg...] + var dst = this.N([], undefined); + var count = this.N(undefined, "y"); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.N({}, [])]; + regs[base + dst] = elems; + break; + } + case 49: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.N({}, "z"); + var pairCount = this.N({}, null); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.N(null, 56)]; + var val = regs[base + this.N("k", undefined)]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50: + { + // obj, key, fn + var obj = regs[base + this.N([], null)]; + var key = regs[base + this.N([], 72)]; + var getterFn = regs[base + this.N({}, [])]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 51: + { + // obj, key, fn + var obj = regs[base + this.N(82, undefined)]; + var key = regs[base + this.N([], null)]; + var setterFn = regs[base + this.N("z", 88)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.N(22, null); + var obj = regs[base + this.N(undefined, undefined)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + x: keys, + X: 0 + }; + break; + } + case 53: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.N(4, undefined); + var iter = regs[base + this.N(58, "y")]; + var exitTarget = this.N("y", []); + if (iter.X >= iter.x.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.x[iter.X++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 54: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + H: this.N(2, []), + V: this.N("k", []) + }); + break; + } + case 55: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 59: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + k: this.N(undefined, 89), + A: this.N([], {}), + f: this.N("x", 31), + C: this.N("x", {}) + }); + break; + } + + // Self-modifying bytecode + case 56: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.N(null, undefined); + var sliceStart = this.N(null, undefined); + var sliceEnd = this.N(38, []); + var pk = this.N({}, "k") ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.q[destPc + (pi - sliceStart)] = (this.q[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 58: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.N(59, null)]; + break; + } + case 57: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 0] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.n; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.B(searchFrame, [], {}); + this.s = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.n = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.V !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.V] = err; + regs[handledFrame + 0] = h.H; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.A] = h.C; + regs[hBase + h.f] = err; + regs[handledFrame + 0] = h.k; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.s = handledFrame + regs[handledFrame + 6]; + this.n = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.n) return this.g[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(CONSTANTS, "x", globals, decodeBytecode(BYTECODE)); +vm.O("k", {}, new Closure(35, [], undefined, undefined, { + T: 0, + l: 12, + p: 0 +}), null // no arguments object / parameter setup for the root frame +, undefined, {}); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--173.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--173.js new file mode 100644 index 00000000..b81b1cda --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--173.js @@ -0,0 +1,966 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(fake_17, fake_15, globals, fake_16, bytecode, constants) { + this.z = bytecode; + this.r = constants; + this.K = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.e = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.G = fake_16; + this.A = []; + this.w = fake_15; + this.c = fake_17; + this.n = 1; + this.q = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) + +// Closure & Frame +function Closure(fake_14, fn) { + this.ad = fn; + this.C = []; + this.m = fake_14; + this.prototype = {}; // <- default prototype object for `new` +} +function Upvalue(absSlot, regs, fake_1) { + this.H = fake_1; + this.A = regs; // shared reference to VM._regs flat array + this.p = absSlot; // absolute index; stable as long as frame is alive + this.a = false; + this.ab = undefined; +} +Upvalue.prototype.y = function (fake_6, fake_7, fake_9, v, fake_8) { + if (this.a) this.ab = v;else this.A[this.p] = v; + this.W = [fake_6, fake_9]; + this.D = fake_7 && fake_8; +}; +VM.prototype.I = function (fake_18, fake_20, fake_19, fake_21) { + this.J = fake_21; + this.g = fake_20; + this.f = fake_18; + return this.z[this.A[this.q + 0]++]; + this.ae = fake_19; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). + +VM.prototype.R = function (fake_33, fake_34, fake_32, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.e; + this.X = fake_33; + this.u = fake_34; + if (!uvs) return; // nothing in this program ever captured a local + this.ag = fake_32; + var regs = this.A; + var hi = fp + regs[fp + 6]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.i("q", "z", {}, "q"); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.E = function (fake_3, fake_4, fake_2, fake_5) { + this.o = fake_3 || fake_5; + this.al = fake_2; + return this.a ? this.ab : this.A[this.p]; + this.O = fake_4; +}; +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.l = function (keyIn, idxIn, fake_30, fake_31) { + this.am = fake_30; + var idx = idxIn ?? this.I({}, {}, [], "z"); + var key = keyIn ?? this.I(undefined, [], null, []); + var v = this.r[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + this.S = fake_31; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.h = function (thisVal, fake_22, fake_23, fake_24, closure, fake_25, args, retDst) { + var fn = closure.ad; + var regs = this.A; + var fp = this.n; + var size = 8 + fn.U; + var end = fp + size; + this.x = fake_24; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + this.Q = [fake_25, fake_22]; + this.V = fake_23; + var base = fp + 8; + regs[fp + 0] = fn.j; + regs[fp + 1] = this.q; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.k; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.j; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.n = end; + if (args) { + if (fn.ak) { + var restSlot = fn.k - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.U; a++) regs[base + a] = args[a]; + } + if (fn.k < fn.U) regs[base + fn.k] = args; + } + this.q = fp; +}; +VM.prototype.t = function (fake_27, fake_26, fake_29, slot, fake_28, fp) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.A[fp + 7] + slot; + this.ah = fake_27 || fake_26; + this.F = fake_28 && fake_29; + var uvs = this.e || (this.e = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, this.A, [])); +}; +Upvalue.prototype.i = function (fake_13, fake_12, fake_11, fake_10) { + this.ab = this.A[this.p]; + this.B = [fake_13, fake_12]; + this.a = true; + this.Y = fake_11; + this.L = fake_10; +}; +VM.prototype.P = function (thisVal, fake_37, fake_35, fake_38, closure, fake_36, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.h(thisVal, [], null, undefined, closure, "x", args, 0); + this.aa = fake_37; + this.d = fake_38; + while (true) { + var fp = this.q; + var regs = this.A; + var bc = this.z; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.z[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.z.length; i++) this.z[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.n; i2++) this.A[i2] = undefined; + op = 39; + regs[fp + 0] = this.z.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 0: + { + var dst = this.I({}, [], "x", 66); + regs[base + dst] = this.l(); + break; + } + case 1: + { + var dst = this.I(4, {}, "y", "k"); + regs[base + dst] = this.I([], null, {}, 64); + break; + } + case 2: + { + var dst = this.I("x", 5, undefined, 91); + var globalName = this.l(); + if (!(globalName in this.K)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.K[globalName]; + break; + } + case 3: + { + var dst = this.I("y", null, undefined, {}); + regs[base + dst] = regs[fp + 4].C[this.I("y", [], undefined, undefined)].E(78, [], undefined, []); + break; + } + case 4: + { + var dst = this.I([], {}, "k", "y"); + regs[base + dst] = regs[fp + 3]; + break; + } + case 5: + { + var dst = this.I([], undefined, [], "k"); + regs[base + dst] = regs[base + this.I(43, null, 67, [])]; + break; + } + case 6: + { + // globals[globalName] = regs[src] + this.K[this.l()] = regs[base + this.I([], undefined, 65, null)]; + break; + } + case 7: + { + var uvIdx = this.I({}, null, undefined, []); + regs[fp + 4].C[uvIdx].y([], {}, [], regs[base + this.I("y", undefined, 89, 54)], {}); + break; + } + case 8: + { + // dst = regs[obj][regs[key]] + var dst = this.I(null, {}, [], "x"); + var obj = regs[base + this.I(29, [], 40, null)]; + var key = regs[base + this.I(8, "y", null, {})]; + regs[base + dst] = obj[key]; + break; + } + case 9: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.I(undefined, undefined, {}, 35)]; + var key = regs[base + this.I({}, 55, null, [])]; + var val = regs[base + this.I([], [], 59, 84)]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 10: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.I({}, {}, "k", "k"); + var obj = regs[base + this.I(null, "z", [], undefined)]; + var key = regs[base + this.I([], null, null, undefined)]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 11: + { + var dst = this.I(null, undefined, 13, 74); + var a = regs[base + this.I({}, null, "z", null)]; + regs[base + dst] = a + regs[base + this.I(null, null, null, 8)]; + break; + } + case 12: + { + var dst = this.I(4, null, {}, undefined); + var a = regs[base + this.I([], null, {}, [])]; + regs[base + dst] = a - regs[base + this.I("k", [], [], undefined)]; + break; + } + case 13: + { + var dst = this.I(null, undefined, [], undefined); + var a = regs[base + this.I(undefined, {}, 96, "q")]; + regs[base + dst] = a * regs[base + this.I(undefined, undefined, [], {})]; + break; + } + case 14: + { + var dst = this.I(undefined, {}, "q", []); + var a = regs[base + this.I(74, 52, {}, 63)]; + regs[base + dst] = a / regs[base + this.I(undefined, "z", [], [])]; + break; + } + case 15: + { + var dst = this.I([], undefined, [], "k"); + var a = regs[base + this.I(34, 11, 14, [])]; + regs[base + dst] = a % regs[base + this.I([], "y", "k", undefined)]; + break; + } + case 60: + { + // Math.pow instead of `**` + var dst = this.I("y", {}, [], undefined); + var a = regs[base + this.I({}, 80, {}, {})]; + regs[base + dst] = Math.pow(a, regs[base + this.I(undefined, 78, {}, {})]); + break; + } + case 16: + { + var dst = this.I(null, {}, 41, {}); + var a = regs[base + this.I(null, [], 45, 54)]; + regs[base + dst] = a & regs[base + this.I(null, 92, null, "x")]; + break; + } + case 17: + { + var dst = this.I(null, undefined, null, null); + var a = regs[base + this.I(undefined, [], "k", [])]; + regs[base + dst] = a | regs[base + this.I([], [], 50, null)]; + break; + } + case 18: + { + var dst = this.I(44, [], null, {}); + var a = regs[base + this.I("x", undefined, null, null)]; + regs[base + dst] = a ^ regs[base + this.I(0, null, "q", {})]; + break; + } + case 19: + { + var dst = this.I(null, "k", null, {}); + var a = regs[base + this.I("k", undefined, "x", {})]; + regs[base + dst] = a << regs[base + this.I(null, 79, null, [])]; + break; + } + case 20: + { + var dst = this.I({}, null, undefined, undefined); + var a = regs[base + this.I({}, "z", null, 82)]; + regs[base + dst] = a >> regs[base + this.I(null, 72, "q", null)]; + break; + } + case 21: + { + var dst = this.I(62, null, undefined, null); + var a = regs[base + this.I([], undefined, null, "q")]; + regs[base + dst] = a >>> regs[base + this.I(null, "z", null, null)]; + break; + } + + // Comparison (dst, src1, src2) + case 22: + { + var dst = this.I("k", {}, [], 22); + var a = regs[base + this.I(null, undefined, undefined, 48)]; + regs[base + dst] = a < regs[base + this.I({}, "x", null, "z")]; + break; + } + case 23: + { + var dst = this.I(28, "k", {}, {}); + var a = regs[base + this.I(88, [], undefined, [])]; + regs[base + dst] = a > regs[base + this.I("z", "y", [], undefined)]; + break; + } + case 24: + { + var dst = this.I([], undefined, [], "x"); + var a = regs[base + this.I(89, [], "k", [])]; + regs[base + dst] = a <= regs[base + this.I([], "x", null, null)]; + break; + } + case 25: + { + var dst = this.I({}, [], {}, []); + var a = regs[base + this.I(12, "q", null, {})]; + regs[base + dst] = a >= regs[base + this.I([], null, {}, null)]; + break; + } + case 26: + { + var dst = this.I(undefined, null, undefined, "y"); + var a = regs[base + this.I([], [], undefined, {})]; + regs[base + dst] = a === regs[base + this.I([], {}, {}, "z")]; + break; + } + case 27: + { + var dst = this.I({}, null, "x", undefined); + var a = regs[base + this.I([], undefined, null, "y")]; + regs[base + dst] = a !== regs[base + this.I(14, "z", [], [])]; + break; + } + case 28: + { + var dst = this.I("z", null, 66, []); + var a = regs[base + this.I({}, null, 88, 0)]; + regs[base + dst] = a == regs[base + this.I([], undefined, undefined, 67)]; + break; + } + case 29: + { + var dst = this.I([], "q", null, "z"); + var a = regs[base + this.I(null, null, undefined, "k")]; + regs[base + dst] = a != regs[base + this.I(null, {}, null, [])]; + break; + } + case 30: + { + var dst = this.I(undefined, [], {}, null); + var a = regs[base + this.I(null, 97, undefined, 17)]; + regs[base + dst] = a in regs[base + this.I({}, undefined, 11, undefined)]; + break; + } + case 31: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.I([], {}, "y", "k"); + var obj = regs[base + this.I([], "k", "k", [])]; + regs[base + dst] = obj instanceof regs[base + this.I("k", 69, null, 29)]; + break; + } + + // Unary (dst, src) + case 32: + { + var dst = this.I("z", undefined, undefined, undefined); + regs[base + dst] = -regs[base + this.I(39, {}, 96, null)]; + break; + } + case 33: + { + var dst = this.I([], [], 31, "x"); + regs[base + dst] = +regs[base + this.I({}, "y", "q", null)]; + break; + } + case 34: + { + var dst = this.I("y", "q", 59, null); + regs[base + dst] = !regs[base + this.I(68, undefined, 75, [])]; + break; + } + case 35: + { + var dst = this.I(null, undefined, [], "x"); + regs[base + dst] = ~regs[base + this.I(71, "z", [], 1)]; + break; + } + case 36: + { + var dst = this.I(null, {}, undefined, null); + regs[base + dst] = typeof regs[base + this.I({}, null, [], 44)]; + break; + } + case 37: + { + var dst = this.I("y", "q", {}, "y"); + this.I([], [], 42, "z"); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 38: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.I(undefined, [], null, "x"); + var name = this.l(); + var val = Object.prototype.hasOwnProperty.call(this.K, name) ? this.K[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39: + regs[fp + 0] = this.I(40, undefined, 39, 28); + break; + case 40: + { + var src = this.I(undefined, {}, [], undefined); + var target = this.I(undefined, {}, undefined, null); + if (!regs[base + src]) regs[fp + 0] = target; + break; + } + case 41: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.I([], undefined, {}, null); + var target = this.I(null, "k", 45, 6); + if (regs[base + src]) regs[fp + 0] = target; + break; + } + + // Calls + case 42: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.I(undefined, {}, 26, undefined); + var callee = regs[base + this.I(null, undefined, "z", [])]; + var argc = this.I({}, undefined, undefined, {}); + var args; + if (argc === 65535) { + args = regs[base + this.I([], 46, null, "q")]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.I("q", [], null, [])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.h(this.K, [], 1, [], closure, 78, args, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 43: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.I("k", 76, 31, undefined); + var receiver = regs[base + this.I("x", {}, undefined, {})]; + var callee = regs[base + this.I(39, undefined, 83, 76)]; + var argc = this.I(53, "y", undefined, "k"); + var args; + if (argc === 65535) { + args = regs[base + this.I(44, null, null, {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.I({}, [], {}, null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.h(receiver, "x", 81, undefined, closure, [], args, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 44: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.I([], 86, {}, "k"); + var callee = regs[base + this.I("y", [], "x", null)]; + var argc = this.I("z", [], [], null); + var args; + if (argc === 65535) { + args = regs[base + this.I("k", 32, [], 27)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.I(undefined, 95, null, 7)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.h(Object.create(closure.prototype || null), {}, undefined, "z", closure, [], args, dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 45: + { + var retVal = regs[base + this.I(undefined, [], [], null)]; + this.R({}, [], {}, fp); // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.n = fp; + this.q = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 46: + throw regs[base + this.I("q", null, [], "k")]; + + // Closures + case 47: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.I({}, "y", [], "q"); + var startPc = this.I([], undefined, [], []); + var paramCount = this.I(17, 85, null, {}); + var regCount = this.I([], null, 15, 36); + var uvCount = this.I([], [], undefined, undefined); + var hasRest = this.I(null, 93, null, undefined); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.I("z", {}, null, undefined); + var uvIndex = this.I({}, "k", {}, null); + uvDescs[i] = { + s: isLocalRaw, + N: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + k: paramCount, + U: regCount, + j: startPc, + ak: hasRest + }; + var closure = new Closure(73, fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.s) { + closure.C.push(this.t("y", 42, 61, uvd.N, undefined, fp)); + } else { + closure.C.push(regs[fp + 4].C[uvd.N]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM("q", null, self.K, undefined, self.z, self.r).P(this == null ? self.K : this, 6, null, {}, c, {}, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 48: + { + // dst, count, [elemReg...] + var dst = this.I(91, [], undefined, undefined); + var count = this.I({}, "k", [], undefined); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.I(3, "k", [], [])]; + regs[base + dst] = elems; + break; + } + case 49: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.I(undefined, null, {}, undefined); + var pairCount = this.I([], "k", [], []); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.I({}, "q", undefined, "y")]; + var val = regs[base + this.I(undefined, {}, "q", [])]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50: + { + // obj, key, fn + var obj = regs[base + this.I([], "k", [], null)]; + var key = regs[base + this.I(null, 59, [], 65)]; + var getterFn = regs[base + this.I([], "y", undefined, "y")]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 51: + { + // obj, key, fn + var obj = regs[base + this.I([], "k", undefined, undefined)]; + var key = regs[base + this.I(null, null, {}, 42)]; + var setterFn = regs[base + this.I({}, [], 59, undefined)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.I("x", 55, null, 50); + var obj = regs[base + this.I(69, undefined, 83, 55)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + T: keys, + ac: 0 + }; + break; + } + case 53: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.I([], {}, null, "z"); + var iter = regs[base + this.I({}, undefined, undefined, [])]; + var exitTarget = this.I(null, 32, undefined, {}); + if (iter.ac >= iter.T.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.T[iter.ac++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 54: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + af: this.I([], {}, "y", "y"), + b: this.I(undefined, 8, "k", null) + }); + break; + } + case 55: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 59: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + M: this.I(undefined, {}, "k", null), + Z: this.I(undefined, {}, 95, null), + v: this.I("z", 50, [], undefined), + aj: this.I(undefined, null, 81, []) + }); + break; + } + + // Self-modifying bytecode + case 56: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.I(undefined, "y", {}, "x"); + var sliceStart = this.I({}, [], null, 46); + var sliceEnd = this.I(undefined, {}, {}, "q"); + var pk = this.I({}, undefined, "x", []) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.z[destPc + (pi - sliceStart)] = (this.z[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 58: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.I(undefined, {}, 58, 43)]; + break; + } + case 57: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 0] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.q; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.R([], [], null, searchFrame); + this.n = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.q = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.b !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.b] = err; + regs[handledFrame + 0] = h.af; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.Z] = h.aj; + regs[hBase + h.v] = err; + regs[handledFrame + 0] = h.M; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.n = handledFrame + regs[handledFrame + 6]; + this.q = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.q) return this.A[0]; + } + this.ai = fake_36 || fake_35; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM({}, {}, globals, "z", decodeBytecode(BYTECODE), CONSTANTS); +vm.P(undefined, 38, 34, undefined, new Closure(null, { + k: 0, + U: 12, + j: 0 +}), "y", null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--174.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--174.js new file mode 100644 index 00000000..c9321783 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--classObfuscation--174.js @@ -0,0 +1,946 @@ +function VM(constants, bytecode, fake_13, fake_15, fake_14, fake_12, globals) { + this.G = bytecode; + this.w = constants; + this.Z = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.E = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.r = fake_14; + this.O = []; + this.z = fake_12 && fake_15; + this.M = fake_13; + this.L = 1; + this.U = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +// Closure & Frame +function Closure(fake_10, fake_11, fn) { + this.g = [fake_10, fake_11]; + this.l = fn; + this.D = []; + this.prototype = {}; // <- default prototype object for `new` +} +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(fake_1, fake_2, regs, absSlot, fake_3) { + this.O = regs; // shared reference to VM._regs flat array + this.u = fake_2 || fake_3; + this.j = fake_1; + this.q = absSlot; // absolute index; stable as long as frame is alive + this.b = false; + this.a = undefined; +} +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CLOSURE_MAP = new WeakMap(); +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +Upvalue.prototype.V = function (fake_5, fake_4, fake_6) { + return this.b ? this.a : this.O[this.q]; + this.C = fake_6; + this.Y = fake_4; + this.i = fake_5; +}; +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.c = function (closure, fake_20, retDst, args, fake_19, thisVal, fake_18) { + var fn = closure.l; + var regs = this.O; + var fp = this.L; + var size = 8 + fn.n; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 8; + regs[fp + 0] = fn.aa; + this.X = fake_20; + regs[fp + 1] = this.U; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.h; + this.W = fake_19; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.aa; + this.B = fake_18; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.L = end; + if (args) { + if (fn.R) { + var restSlot = fn.h - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.n; a++) regs[base + a] = args[a]; + } + if (fn.h < fn.n) regs[base + fn.h] = args; + } + this.U = fp; +}; +VM.prototype.ab = function (fake_25, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.E; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this.O; + var hi = fp + regs[fp + 6]; + this.v = fake_25; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.m(null, {}); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.J = function (fp, fake_22, fake_21, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.O[fp + 7] + slot; + var uvs = this.E || (this.E = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(undefined, null, this.O, absSlot, [])); + this.H = [fake_22, fake_21]; +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.k = function (idxIn, fake_24, fake_23, keyIn) { + var idx = idxIn ?? this.o(undefined, 53); + var key = keyIn ?? this.o(null, "y"); + var v = this.w[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + this.d = [fake_24, fake_23]; + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +Upvalue.prototype.F = function (fake_7, v) { + this.K = fake_7; + if (this.b) this.a = v;else this.O[this.q] = v; +}; +Upvalue.prototype.m = function (fake_9, fake_8) { + this.a = this.O[this.q]; + this.b = true; + this.A = [fake_8, fake_9]; +}; +VM.prototype.o = function (fake_17, fake_16) { + this.T = [fake_16, fake_17]; + return this.G[this.O[this.U + 0]++]; +}; +VM.prototype.t = function (thisVal, fake_26, args, fake_28, closure, fake_27) { + var now = () => { + return performance.now(); + }; + this.f = fake_26; + var lastTime = now(); + this.c(closure, undefined, 0, args, "x", thisVal, undefined); + while (true) { + var fp = this.U; + var regs = this.O; + var bc = this.G; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.G[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.G.length; i++) this.G[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.L; i2++) this.O[i2] = undefined; + op = 39; + regs[fp + 0] = this.G.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 0: + { + var dst = this.o([], {}); + regs[base + dst] = this.k(); + break; + } + case 1: + { + var dst = this.o([], 12); + regs[base + dst] = this.o("y", null); + break; + } + case 2: + { + var dst = this.o(undefined, undefined); + var globalName = this.k(); + if (!(globalName in this.Z)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.Z[globalName]; + break; + } + case 3: + { + var dst = this.o(undefined, undefined); + regs[base + dst] = regs[fp + 4].D[this.o([], 81)].V("q", "y", 9); + break; + } + case 4: + { + var dst = this.o([], undefined); + regs[base + dst] = regs[fp + 3]; + break; + } + case 5: + { + var dst = this.o("k", []); + regs[base + dst] = regs[base + this.o(82, [])]; + break; + } + case 6: + { + // globals[globalName] = regs[src] + this.Z[this.k()] = regs[base + this.o({}, "z")]; + break; + } + case 7: + { + var uvIdx = this.o(undefined, undefined); + regs[fp + 4].D[uvIdx].F(57, regs[base + this.o(44, undefined)]); + break; + } + case 8: + { + // dst = regs[obj][regs[key]] + var dst = this.o({}, null); + var obj = regs[base + this.o(undefined, undefined)]; + var key = regs[base + this.o([], 64)]; + regs[base + dst] = obj[key]; + break; + } + case 9: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.o({}, null)]; + var key = regs[base + this.o("z", undefined)]; + var val = regs[base + this.o(null, {})]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 10: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.o(85, undefined); + var obj = regs[base + this.o(undefined, [])]; + var key = regs[base + this.o("q", 64)]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 11: + { + var dst = this.o("z", []); + var a = regs[base + this.o(70, [])]; + regs[base + dst] = a + regs[base + this.o([], 95)]; + break; + } + case 12: + { + var dst = this.o(null, {}); + var a = regs[base + this.o(null, 88)]; + regs[base + dst] = a - regs[base + this.o([], {})]; + break; + } + case 13: + { + var dst = this.o("x", []); + var a = regs[base + this.o("k", {})]; + regs[base + dst] = a * regs[base + this.o(undefined, null)]; + break; + } + case 14: + { + var dst = this.o(null, undefined); + var a = regs[base + this.o("z", 3)]; + regs[base + dst] = a / regs[base + this.o("k", 77)]; + break; + } + case 15: + { + var dst = this.o([], undefined); + var a = regs[base + this.o(null, [])]; + regs[base + dst] = a % regs[base + this.o(null, "k")]; + break; + } + case 60: + { + // Math.pow instead of `**` + var dst = this.o(undefined, 37); + var a = regs[base + this.o("y", {})]; + regs[base + dst] = Math.pow(a, regs[base + this.o([], "x")]); + break; + } + case 16: + { + var dst = this.o([], "x"); + var a = regs[base + this.o({}, "z")]; + regs[base + dst] = a & regs[base + this.o("y", 15)]; + break; + } + case 17: + { + var dst = this.o(null, 51); + var a = regs[base + this.o([], [])]; + regs[base + dst] = a | regs[base + this.o(null, undefined)]; + break; + } + case 18: + { + var dst = this.o([], {}); + var a = regs[base + this.o({}, {})]; + regs[base + dst] = a ^ regs[base + this.o({}, [])]; + break; + } + case 19: + { + var dst = this.o(32, null); + var a = regs[base + this.o(56, [])]; + regs[base + dst] = a << regs[base + this.o(87, {})]; + break; + } + case 20: + { + var dst = this.o([], null); + var a = regs[base + this.o(74, [])]; + regs[base + dst] = a >> regs[base + this.o({}, "y")]; + break; + } + case 21: + { + var dst = this.o([], []); + var a = regs[base + this.o({}, null)]; + regs[base + dst] = a >>> regs[base + this.o(undefined, 50)]; + break; + } + + // Comparison (dst, src1, src2) + case 22: + { + var dst = this.o(null, []); + var a = regs[base + this.o(undefined, 3)]; + regs[base + dst] = a < regs[base + this.o(undefined, "k")]; + break; + } + case 23: + { + var dst = this.o(59, null); + var a = regs[base + this.o("y", 48)]; + regs[base + dst] = a > regs[base + this.o([], {})]; + break; + } + case 24: + { + var dst = this.o(null, 17); + var a = regs[base + this.o([], 98)]; + regs[base + dst] = a <= regs[base + this.o("z", "k")]; + break; + } + case 25: + { + var dst = this.o(undefined, 82); + var a = regs[base + this.o([], [])]; + regs[base + dst] = a >= regs[base + this.o(undefined, undefined)]; + break; + } + case 26: + { + var dst = this.o(null, {}); + var a = regs[base + this.o(undefined, "k")]; + regs[base + dst] = a === regs[base + this.o(74, [])]; + break; + } + case 27: + { + var dst = this.o(undefined, "k"); + var a = regs[base + this.o(null, {})]; + regs[base + dst] = a !== regs[base + this.o(3, null)]; + break; + } + case 28: + { + var dst = this.o({}, []); + var a = regs[base + this.o(29, undefined)]; + regs[base + dst] = a == regs[base + this.o({}, "x")]; + break; + } + case 29: + { + var dst = this.o({}, []); + var a = regs[base + this.o(null, "z")]; + regs[base + dst] = a != regs[base + this.o([], undefined)]; + break; + } + case 30: + { + var dst = this.o("k", "k"); + var a = regs[base + this.o([], 66)]; + regs[base + dst] = a in regs[base + this.o(null, null)]; + break; + } + case 31: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.o(undefined, null); + var obj = regs[base + this.o("q", null)]; + regs[base + dst] = obj instanceof regs[base + this.o(undefined, [])]; + break; + } + + // Unary (dst, src) + case 32: + { + var dst = this.o([], 19); + regs[base + dst] = -regs[base + this.o(undefined, 30)]; + break; + } + case 33: + { + var dst = this.o("x", {}); + regs[base + dst] = +regs[base + this.o(60, 97)]; + break; + } + case 34: + { + var dst = this.o([], null); + regs[base + dst] = !regs[base + this.o(null, {})]; + break; + } + case 35: + { + var dst = this.o(6, null); + regs[base + dst] = ~regs[base + this.o(null, undefined)]; + break; + } + case 36: + { + var dst = this.o("y", 22); + regs[base + dst] = typeof regs[base + this.o({}, 11)]; + break; + } + case 37: + { + var dst = this.o({}, null); + this.o("q", "x"); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 38: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.o(undefined, "q"); + var name = this.k(); + var val = Object.prototype.hasOwnProperty.call(this.Z, name) ? this.Z[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39: + regs[fp + 0] = this.o("z", "z"); + break; + case 40: + { + var src = this.o("y", undefined); + var target = this.o([], undefined); + if (!regs[base + src]) regs[fp + 0] = target; + break; + } + case 41: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.o("k", "q"); + var target = this.o(53, []); + if (regs[base + src]) regs[fp + 0] = target; + break; + } + + // Calls + case 42: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.o([], null); + var callee = regs[base + this.o({}, {})]; + var argc = this.o(undefined, []); + var args; + if (argc === 65535) { + args = regs[base + this.o(null, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.o(null, undefined)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.c(closure, "x", dst << 1, args, [], this.Z, null); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 43: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.o(null, []); + var receiver = regs[base + this.o({}, undefined)]; + var callee = regs[base + this.o(undefined, null)]; + var argc = this.o(null, null); + var args; + if (argc === 65535) { + args = regs[base + this.o({}, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.o(null, "z")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.c(closure, 57, dst << 1, args, [], receiver, {}); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 44: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.o([], "y"); + var callee = regs[base + this.o([], "y")]; + var argc = this.o(null, "z"); + var args; + if (argc === 65535) { + args = regs[base + this.o(20, [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.o([], null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.c(closure, 30, dst << 1 | 1, args, "q", Object.create(closure.prototype || null), []); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 45: + { + var retVal = regs[base + this.o({}, 13)]; + this.ab(null, fp); // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.L = fp; + this.U = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 46: + throw regs[base + this.o("y", null)]; + + // Closures + case 47: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.o("q", undefined); + var startPc = this.o(undefined, []); + var paramCount = this.o("z", null); + var regCount = this.o([], []); + var uvCount = this.o(undefined, {}); + var hasRest = this.o(89, null); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.o([], []); + var uvIndex = this.o({}, []); + uvDescs[i] = { + I: isLocalRaw, + Q: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + h: paramCount, + n: regCount, + aa: startPc, + R: hasRest + }; + var closure = new Closure(45, undefined, fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.I) { + closure.D.push(this.J(fp, 79, 73, uvd.Q)); + } else { + closure.D.push(regs[fp + 4].D[uvd.Q]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.w, self.G, 2, null, null, undefined, self.Z).t(this == null ? self.Z : this, 36, Array.prototype.slice.call(arguments), 66, c, 70); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 48: + { + // dst, count, [elemReg...] + var dst = this.o({}, 67); + var count = this.o(undefined, "x"); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.o(null, 63)]; + regs[base + dst] = elems; + break; + } + case 49: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.o(91, undefined); + var pairCount = this.o({}, null); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.o([], 79)]; + var val = regs[base + this.o("k", "x")]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50: + { + // obj, key, fn + var obj = regs[base + this.o(null, "q")]; + var key = regs[base + this.o(undefined, undefined)]; + var getterFn = regs[base + this.o([], null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 51: + { + // obj, key, fn + var obj = regs[base + this.o(null, null)]; + var key = regs[base + this.o(undefined, {})]; + var setterFn = regs[base + this.o(93, null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.o(null, undefined); + var obj = regs[base + this.o(null, null)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + x: keys, + N: 0 + }; + break; + } + case 53: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.o({}, undefined); + var iter = regs[base + this.o({}, undefined)]; + var exitTarget = this.o({}, 25); + if (iter.N >= iter.x.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.x[iter.N++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 54: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + e: this.o(undefined, undefined), + ac: this.o(null, 44) + }); + break; + } + case 55: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 59: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + P: this.o(undefined, "q"), + y: this.o(null, "y"), + s: this.o(undefined, null), + p: this.o(null, "z") + }); + break; + } + + // Self-modifying bytecode + case 56: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.o("q", {}); + var sliceStart = this.o({}, 5); + var sliceEnd = this.o(26, null); + var pk = this.o({}, []) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.G[destPc + (pi - sliceStart)] = (this.G[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 58: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.o(undefined, undefined)]; + break; + } + case 57: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 0] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.U; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.ab(undefined, searchFrame); + this.L = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.U = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.ac !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.ac] = err; + regs[handledFrame + 0] = h.e; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.y] = h.p; + regs[hBase + h.s] = err; + regs[handledFrame + 0] = h.P; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.L = handledFrame + regs[handledFrame + 6]; + this.U = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.U) return this.O[0]; + } + this.S = fake_28 || fake_27; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(CONSTANTS, decodeBytecode(BYTECODE), "y", [], {}, [], globals); +vm.t(undefined, undefined, null // no arguments object / parameter setup for the root frame +, undefined, new Closure({}, "k", { + h: 0, + n: 12, + aa: 0 +}), null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--172.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--172.js new file mode 100644 index 00000000..c1ec34ed --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--172.js @@ -0,0 +1,18 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new C([4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237, +2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],U),new B({m:0,v:12,G:0}),void 0, +null); diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--173.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--173.js new file mode 100644 index 00000000..c1ec34ed --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--173.js @@ -0,0 +1,18 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new C([4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237, +2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],U),new B({m:0,v:12,G:0}),void 0, +null); diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--174.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--174.js new file mode 100644 index 00000000..c1ec34ed --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minify--174.js @@ -0,0 +1,18 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new C([4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237, +2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],U),new B({m:0,v:12,G:0}),void 0, +null); diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--172.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--172.js new file mode 100644 index 00000000..f05f057e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--172.js @@ -0,0 +1,18 @@ +function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var c=new WeakMap;function A(a,g){this.g=g;this.e=a;this.o=!1;this.u=void 0}function B(a,g,k){this.q=k;this.v=a;this.h=g;this.m=null;this.g=[];this.s=1;this.n=0}function C(a,g,k){this.t=k;this.i=[];this.E=[g,a];this.prototype={}}function fa(a,g,k){g=a.g[g+7]+k;k=a.m||(a.m=[]);return k[g]||(k[g]=new A(g,a.g))} +B.prototype.d=function(a,g,k,b,h,f){a=g.t;h=this.g;for(var m=this.s,d=8+a.l,e=m+d;h.length>>13)&65535);return g}function Q(a,g,k,b){var h=a.m;if(h){var f=a.g;a.b=k||b;a=g+f[g+6];for(g=f[g+7];g=a.q.length)break;b[f+0]=m+1;m=a.q[m];try{var d=b[f+7];switch(m){case 0:var e=J(a);b[d+e]=D(a);break;case 1:e=J(a);b[d+e]=J(a);break;case 2:e=J(a);var p=D(a);if(!(p in a.h))throw new ReferenceError(`${p} is not defined`);b[d+e]=a.h[p];break;case 3:e=J(a);b[d+e]=b[f+4].i[J(a)].w(void 0);break;case 4:e=J(a);b[d+e]=b[f+3];break;case 5:e=J(a);b[d+e]=b[d+J(a)];break;case 6:a.h[D(a)]=b[d+J(a)];break;case 7:var ha=J(a); +b[f+4].i[ha].c(void 0,b[d+J(a)],null,"z");break;case 8:e=J(a);var n=b[d+J(a)],q=b[d+J(a)];b[d+e]=n[q];break;case 9:n=b[d+J(a)];q=b[d+J(a)];var x=b[d+J(a)];Reflect.set(n,q,x);break;case 10:e=J(a);n=b[d+J(a)];q=b[d+J(a)];b[d+e]=delete n[q];break;case 11:e=J(a);var l=b[d+J(a)];b[d+e]=l+b[d+J(a)];break;case 12:e=J(a);l=b[d+J(a)];b[d+e]=l-b[d+J(a)];break;case 13:e=J(a);l=b[d+J(a)];b[d+e]=l*b[d+J(a)];break;case 14:e=J(a);l=b[d+J(a)];b[d+e]=l/b[d+J(a)];break;case 15:e=J(a);l=b[d+J(a)];b[d+e]=l%b[d+J(a)]; +break;case 60:e=J(a);l=b[d+J(a)];b[d+e]=Math.pow(l,b[d+J(a)]);break;case 16:e=J(a);l=b[d+J(a)];b[d+e]=l&b[d+J(a)];break;case 17:e=J(a);l=b[d+J(a)];b[d+e]=l|b[d+J(a)];break;case 18:e=J(a);l=b[d+J(a)];b[d+e]=l^b[d+J(a)];break;case 19:e=J(a);l=b[d+J(a)];b[d+e]=l<>b[d+J(a)];break;case 21:e=J(a);l=b[d+J(a)];b[d+e]=l>>>b[d+J(a)];break;case 22:e=J(a);l=b[d+J(a)];b[d+e]=lb[d+J(a)];break;case 24:e=J(a); +l=b[d+J(a)];b[d+e]=l<=b[d+J(a)];break;case 25:e=J(a);l=b[d+J(a)];b[d+e]=l>=b[d+J(a)];break;case 26:e=J(a);l=b[d+J(a)];b[d+e]=l===b[d+J(a)];break;case 27:e=J(a);l=b[d+J(a)];b[d+e]=l!==b[d+J(a)];break;case 28:e=J(a);l=b[d+J(a)];b[d+e]=l==b[d+J(a)];break;case 29:e=J(a);l=b[d+J(a)];b[d+e]=l!=b[d+J(a)];break;case 30:e=J(a);l=b[d+J(a)];b[d+e]=l in b[d+J(a)];break;case 31:e=J(a);n=b[d+J(a)];b[d+e]=n instanceof b[d+J(a)];break;case 32:e=J(a);b[d+e]=-b[d+J(a)];break;case 33:e=J(a);b[d+e]=+b[d+J(a)];break; +case 34:e=J(a);b[d+e]=!b[d+J(a)];break;case 35:e=J(a);b[d+e]=~b[d+J(a)];break;case 36:e=J(a);b[d+e]=typeof b[d+J(a)];break;case 37:e=J(a);J(a);b[d+e]=void 0;break;case 38:e=J(a);var T=D(a);x=Object.prototype.hasOwnProperty.call(a.h,T)?a.h[T]:void 0;b[d+e]=typeof x;break;case 39:b[f+0]=J(a);break;case 40:var K=J(a),L=J(a);b[d+K]||(b[f+0]=L);break;case 41:K=J(a);L=J(a);b[d+K]&&(b[f+0]=L);break;case 42:e=J(a);var r=b[d+J(a)],t=J(a);if(t===65535)k=b[d+J(a)];else for(k=Array(t),h=0;h>1):0]=E;break;case 46:throw b[d+J(a)];case 47:e=J(a);var ja=J(a),ka=J(a),la=J(a),W=J(a),ma=J(a),N=Array(W);for(h=0;h=H.x.length?b[f+0]=sa:b[d+e]=H.x[H.B++];break;case 54:var v=b[f+5];v||(b[f+5]=v=[]);v.push({F:J(a),A:J(a)});break;case 55:b[f+5].pop();break;case 59:(v=b[f+5])||(b[f+5]=v=[]);v.push({k:J(a),C:J(a),f:J(a),D:J(a)});break;case 56:var ca=J(a),da=J(a),ta=J(a),I=J(a)^ca|0;for(f=da;f>>13)>>>0;break;case 58:b[f+0]=b[d+J(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+ +" at pc "+(b[f+0]-1));}}catch(y){f=0;for(m=a.n;m;){var w=b[m+5];if(w&&w.length>0){f=m;break}Q(a,m,[],{});a.s=m;m=b[m+1];a.n=m}if(!f)throw y;m=b[f+5].pop();w=b[f+7];m.A!==void 0?(b[w+m.A]=y,b[f+0]=m.F):(b[w+m.C]=m.D,b[w+m.f]=y,b[f+0]=m.k);a.s=f+b[f+6];a.n=f}if(!a.n)return a.g[0]}}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new B(["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],S,[4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40, +8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237,2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7]),new C(35,void 0,{j:0,l:12, +p:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--173.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--173.js new file mode 100644 index 00000000..2a683dd8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--173.js @@ -0,0 +1,18 @@ +function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}function c(a,b,f,h,m){this.z=h;this.r=m;this.h=f;this.e=null;this.g=[];this.w=b;this.c=a;this.n=1;this.q=0}var B=new WeakMap;function C(a){this.C=a;this.i=[];this.prototype={}}function D(a,b){this.g=b;this.p=a;this.a=!1;this.l=void 0}D.prototype.y=function(a,b,f,h){this.a?this.l=h:this.g[this.p]=h};function E(a,b){a.f=b;return a.z[a.g[a.q+0]++]} +function K(a,b){var f=a.e;if(f){var h=a.g;a=b+h[b+6];for(b=h[b+7];b>>13)&65535);return b} +function S(a,b,f,h,m,k,l,d){var e=m.C,p=a.g,r=a.n,n=8+e.j,q=r+n;for(a.x=h;p.length=a.z.length)break;b[f+0]=l+1;l=a.z[l];try{var d=b[f+7];switch(l){case 0:var e=E(a,{});b[d+e]=L(a);break;case 1:e=E(a,4);b[d+e]=E(a,[]);break;case 2:e=E(a,"x");var p=L(a);if(!(p in a.h))throw new ReferenceError(`${p} is not defined`);b[d+e]=a.h[p];break;case 3:e=E(a,"y");b[d+e]=b[f+4].i[E(a,"y")].E(78,[],void 0,[]);break;case 4:e=E(a,[]);b[d+e]=b[f+3];break;case 5:e=E(a,[]);b[d+e]=b[d+E(a,43)];break;case 6:a.h[L(a)]= +b[d+E(a,[])];break;case 7:var r=E(a,{});b[f+4].i[r].y([],{},[],b[d+E(a,"y")],{});break;case 8:e=E(a,null);var n=b[d+E(a,29)],q=b[d+E(a,8)];b[d+e]=n[q];break;case 9:n=b[d+E(a)];q=b[d+E(a,{})];var y=b[d+E(a,[])];Reflect.set(n,q,y);break;case 10:e=E(a,{});n=b[d+E(a,null)];q=b[d+E(a,[])];b[d+e]=delete n[q];break;case 11:e=E(a,null);var g=b[d+E(a,{})];b[d+e]=g+b[d+E(a,null)];break;case 12:e=E(a,4);g=b[d+E(a,[])];b[d+e]=g-b[d+E(a,"k")];break;case 13:e=E(a,null);g=b[d+E(a)];b[d+e]=g*b[d+E(a)];break;case 14:e= +E(a);g=b[d+E(a,74)];b[d+e]=g/b[d+E(a)];break;case 15:e=E(a,[]);g=b[d+E(a,34)];b[d+e]=g%b[d+E(a,[])];break;case 60:e=E(a,"y");g=b[d+E(a,{})];b[d+e]=Math.pow(g,b[d+E(a)]);break;case 16:e=E(a,null);g=b[d+E(a,null)];b[d+e]=g&b[d+E(a,null)];break;case 17:e=E(a,null);g=b[d+E(a)];b[d+e]=g|b[d+E(a,[])];break;case 18:e=E(a,44);g=b[d+E(a,"x")];b[d+e]=g^b[d+E(a,0)];break;case 19:e=E(a,null);g=b[d+E(a,"k")];b[d+e]=g<>b[d+E(a,null)];break;case 21:e= +E(a,62);g=b[d+E(a,[])];b[d+e]=g>>>b[d+E(a,null)];break;case 22:e=E(a,"k");g=b[d+E(a,null)];b[d+e]=gb[d+E(a,"z")];break;case 24:e=E(a,[]);g=b[d+E(a,89)];b[d+e]=g<=b[d+E(a,[])];break;case 25:e=E(a,{});g=b[d+E(a,12)];b[d+e]=g>=b[d+E(a,[])];break;case 26:e=E(a);g=b[d+E(a,[])];b[d+e]=g===b[d+E(a,[])];break;case 27:e=E(a,{});g=b[d+E(a,[])];b[d+e]=g!==b[d+E(a,14)];break;case 28:e=E(a,"z");g=b[d+E(a,{})];b[d+e]=g==b[d+E(a,[])];break;case 29:e= +E(a,[]);g=b[d+E(a,null)];b[d+e]=g!=b[d+E(a,null)];break;case 30:e=E(a);g=b[d+E(a,null)];b[d+e]=g in b[d+E(a,{})];break;case 31:e=E(a,[]);n=b[d+E(a,[])];b[d+e]=n instanceof b[d+E(a,"k")];break;case 32:e=E(a,"z");b[d+e]=-b[d+E(a,39)];break;case 33:e=E(a,[]);b[d+e]=+b[d+E(a,{})];break;case 34:e=E(a,"y");b[d+e]=!b[d+E(a,68)];break;case 35:e=E(a,null);b[d+e]=~b[d+E(a,71)];break;case 36:e=E(a,null);b[d+e]=typeof b[d+E(a,{})];break;case 37:e=E(a,"y");E(a,[]);b[d+e]=void 0;break;case 38:e=E(a);var V=L(a); +y=Object.prototype.hasOwnProperty.call(a.h,V)?a.h[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=E(a,40);break;case 40:var M=E(a),N=E(a);b[d+M]||(b[f+0]=N);break;case 41:M=E(a,[]);N=E(a,null);b[d+M]&&(b[f+0]=N);break;case 42:e=E(a);var t=b[d+E(a,null)],u=E(a,{});if(u===65535)m=b[d+E(a,[])];else for(m=Array(u),k=0;k>1):0]=F;break;case 46:throw b[d+E(a,"q")];case 47:e=E(a,{});var ja=E(a,[]),ka=E(a,17),la=E(a,[]),Y=E(a,[]),ma=E(a,null),P=Array(Y);for(k=0;k=I.o.length?b[f+0]=sa:b[d+e]=I.o[I.u++];break;case 54:var w=b[f+5];w||(b[f+5]=w=[]);w.push({D:E(a,[]),b:E(a)});break;case 55:b[f+5].pop();break;case 59:(w=b[f+5])||(b[f+5]=w=[]);w.push({A:E(a),B:E(a),H:E(a,"z"),F:E(a)});break;case 56:var ea=E(a),fa=E(a,{}),ta=E(a),J=E(a,{})^ea|0;for(f=fa;f>>13)>>>0;break;case 58:b[f+0]=b[d+E(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+ +l+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(l=a.q;l;){var x=b[l+5];if(x&&x.length>0){f=l;break}K(a,l);a.n=l;l=b[l+1];a.q=l}if(!f)throw z;l=b[f+5].pop();x=b[f+7];l.b!==void 0?(b[x+l.b]=z,b[f+0]=l.D):(b[x+l.B]=l.F,b[x+l.H]=z,b[f+0]=l.A);a.n=f+b[f+6];a.q=f}if(!a.q)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new c({},{},U,[4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7, +40,8,237,2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"]),void 0,void 0,new C({k:0, +j:12,v:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--174.js b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--174.js new file mode 100644 index 00000000..eb9acc05 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/exceptionRoutes--minifyClassObfuscation--174.js @@ -0,0 +1,20 @@ +function b(a,d,h,f,l,k){this.j=d;this.w=a;this.i=k;this.E=null;this.r=f;this.g=[];this.z=l&&h;this.m=1;this.h=0}function x(a){this.F=a;this.l=[];this.prototype={}}function ba(a,d){this.g=a;this.q=d;this.b=!1;this.a=void 0}function ca(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(d){return d.charCodeAt(0)})}var B=new WeakMap;function da(a){return a.b?a.a:a.g[a.q]} +b.prototype.c=function(a,d,h,f,l,k){d=a.F;l=this.g;for(var c=this.m,e=8+d.n,t=c+e;l.length>>13)&65535);return h}; +function fa(a,d){a.b?a.a=d:a.g[a.q]=d}function O(a){return a.j[a.g[a.h+0]++]} +b.prototype.t=function(a,d,h,f,l){var k;this.f=d;for(this.c(l,void 0,0,h,"x",a,void 0);;){d=this.h;a=this.g;f=a[d+0];if(f>=this.j.length)break;a[d+0]=f+1;f=this.j[f];try{var c=a[d+7];switch(f){case 0:var e=O(this);a[c+e]=this.k();break;case 1:e=O(this);a[c+e]=O(this);break;case 2:e=O(this);var t=this.k();if(!(t in this.i))throw new ReferenceError(`${t} is not defined`);a[c+e]=this.i[t];break;case 3:e=O(this);a[c+e]=da(a[d+4].l[O(this)]);break;case 4:e=O(this);a[c+e]=a[d+3];break;case 5:e=O(this); +a[c+e]=a[c+O(this)];break;case 6:this.i[this.k()]=a[c+O(this)];break;case 7:var p=O(this);fa(a[d+4].l[p],a[c+O(this)]);break;case 8:e=O(this);var m=a[c+O(this)],n=a[c+O(this)];a[c+e]=m[n];break;case 9:m=a[c+O(this)];n=a[c+O(this)];var y=a[c+O(this)];Reflect.set(m,n,y);break;case 10:e=O(this);m=a[c+O(this)];n=a[c+O(this)];a[c+e]=delete m[n];break;case 11:e=O(this);var g=a[c+O(this)];a[c+e]=g+a[c+O(this)];break;case 12:e=O(this);g=a[c+O(this)];a[c+e]=g-a[c+O(this)];break;case 13:e=O(this);g=a[c+O(this)]; +a[c+e]=g*a[c+O(this)];break;case 14:e=O(this);g=a[c+O(this)];a[c+e]=g/a[c+O(this)];break;case 15:e=O(this);g=a[c+O(this)];a[c+e]=g%a[c+O(this)];break;case 60:e=O(this);g=a[c+O(this)];a[c+e]=Math.pow(g,a[c+O(this)]);break;case 16:e=O(this);g=a[c+O(this)];a[c+e]=g&a[c+O(this)];break;case 17:e=O(this);g=a[c+O(this)];a[c+e]=g|a[c+O(this)];break;case 18:e=O(this);g=a[c+O(this)];a[c+e]=g^a[c+O(this)];break;case 19:e=O(this);g=a[c+O(this)];a[c+e]=g<>a[c+O(this)];break;case 21:e=O(this);g=a[c+O(this)];a[c+e]=g>>>a[c+O(this)];break;case 22:e=O(this);g=a[c+O(this)];a[c+e]=ga[c+O(this)];break;case 24:e=O(this);g=a[c+O(this)];a[c+e]=g<=a[c+O(this)];break;case 25:e=O(this);g=a[c+O(this)];a[c+e]=g>=a[c+O(this)];break;case 26:e=O(this);g=a[c+O(this)];a[c+e]=g===a[c+O(this)];break;case 27:e=O(this);g=a[c+O(this)];a[c+e]=g!==a[c+O(this)];break;case 28:e=O(this);g=a[c+O(this)];a[c+e]=g== +a[c+O(this)];break;case 29:e=O(this);g=a[c+O(this)];a[c+e]=g!=a[c+O(this)];break;case 30:e=O(this);g=a[c+O(this)];a[c+e]=g in a[c+O(this)];break;case 31:e=O(this);m=a[c+O(this)];a[c+e]=m instanceof a[c+O(this)];break;case 32:e=O(this);a[c+e]=-a[c+O(this)];break;case 33:e=O(this);a[c+e]=+a[c+O(this)];break;case 34:e=O(this);a[c+e]=!a[c+O(this)];break;case 35:e=O(this);a[c+e]=~a[c+O(this)];break;case 36:e=O(this);a[c+e]=typeof a[c+O(this)];break;case 37:e=O(this);O(this);a[c+e]=void 0;break;case 38:e= +O(this);var Q=this.k();y=Object.prototype.hasOwnProperty.call(this.i,Q)?this.i[Q]:void 0;a[c+e]=typeof y;break;case 39:a[d+0]=O(this);break;case 40:var I=O(this),J=O(this);a[c+I]||(a[d+0]=J);break;case 41:I=O(this);J=O(this);a[c+I]&&(a[d+0]=J);break;case 42:e=O(this);var q=a[c+O(this)],r=O(this);if(r===65535)h=a[c+O(this)];else for(h=Array(r),k=0;k>1):0]=D;break;case 46:throw a[c+O(this)];case 47:e=O(this);var ia=O(this),ja=O(this),ka=O(this),T=O(this),la=O(this),L=Array(T);for(k=0;k=G.x.length?a[d+0]=ra:a[c+e]=G.x[G.u++];break;case 54:var v=a[d+5];v||(a[d+5]=v=[]);v.push({e:O(this),A:O(this)});break;case 55:a[d+5].pop();break;case 59:(v=a[d+5])||(a[d+5]=v=[]);v.push({C:O(this),y:O(this),s:O(this),p:O(this)});break;case 56:var Z=O(this),aa=O(this),sa=O(this),H=O(this)^Z|0;for(d=aa;d>>13)>>>0;break;case 58:a[d+0]=a[c+O(this)];break;case 57:debugger;break; +default:throw Error("Unknown opcode: "+f+" at pc "+(a[d+0]-1));}}catch(z){d=0;for(f=this.h;f;){var w=a[f+5];if(w&&w.length>0){d=f;break}C(this,f);this.m=f;this.h=f=a[f+1]}if(!d)throw z;f=a[d+5].pop();w=a[d+7];f.A!==void 0?(a[w+f.A]=z,a[d+0]=f.e):(a[w+f.y]=f.p,a[w+f.s]=z,a[d+0]=f.C);this.m=d+a[d+6];this.h=d}if(!this.h)return this.g[0]}};var P=globalThis;typeof window!=="undefined"&&(P.window=window,P.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(P.module=module,P.exports=typeof exports!=="undefined"?exports:void 0); +(new b(["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],[4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185, +2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237,2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],[],{},[],P)).t(void 0,void 0,null, +void 0,new x({o:0,n:12,v:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/expected.json b/test/vm/jsconfuser-vm/fixtures/exception-region/expected.json new file mode 100644 index 00000000..4412213c --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/expected.json @@ -0,0 +1 @@ +[1, 2, 3, 4, ["try:0", "catch:SyntaxError", "finally:0", "try:1", "catch:TypeError", "finally:1", "try:2", "catch:Error", "finally:2", "try:3", "finally:3"]] diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/provenance.json b/test/vm/jsconfuser-vm/fixtures/exception-region/provenance.json new file mode 100644 index 00000000..b47c8987 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/provenance.json @@ -0,0 +1,406 @@ +{ + "schemaVersion": "exception-region-parser-data.v1", + "encoderPin": "20c5b96bf57337c568579352758d7685358650de", + "boundary": "Frozen source/encoded parser data; no native qualification. Raw encoded bytes are preserved exactly.", + "records": [ + { + "id": "exceptionRoutes--baseline--172", + "profile": "baseline", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--baseline--172.js", + "generationRecordSha256": "0a38523064be996028be429839df1b142c03ac99fb058a837e81241829d30a2b", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 172, + "state": 172 + } + }, + { + "id": "exceptionRoutes--baseline--173", + "profile": "baseline", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--baseline--173.js", + "generationRecordSha256": "bd534ff3ae4d8ebb3fa83472ab76cbf1201ae2d595e1bd1a305dbcad6fde134c", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 173, + "state": 173 + } + }, + { + "id": "exceptionRoutes--baseline--174", + "profile": "baseline", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--baseline--174.js", + "generationRecordSha256": "b8fd5117819ae96ee6184f13c454836f797b42138f552b4a37cbfbbe4eaebc2f", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 174, + "state": 174 + } + }, + { + "id": "exceptionRoutes--minify--172", + "profile": "minify", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--minify--172.js", + "generationRecordSha256": "19e4b3cbf0da22b6f5d9cfcbc38cb40026a9a0cdbd905f45f43eb32b9d5b00cd", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 172, + "state": 172 + } + }, + { + "id": "exceptionRoutes--minify--173", + "profile": "minify", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--minify--173.js", + "generationRecordSha256": "8f19ab2da328340633018cedad368145645b5c3266016008b5e8571bac5ff487", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 173, + "state": 173 + } + }, + { + "id": "exceptionRoutes--minify--174", + "profile": "minify", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--minify--174.js", + "generationRecordSha256": "1390d7db3a193175a32433d89fba8ec8d16219d0134cbb7ef32d421887d39e29", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 174, + "state": 174 + } + }, + { + "id": "exceptionRoutes--classObfuscation--172", + "profile": "classObfuscation", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--classObfuscation--172.js", + "generationRecordSha256": "eadf209dd3eb0e4fe0ff8acccb89aa8d57fb85abae9c799b69c40173c2c26af1", + "rng": { + "algorithm": "mulberry32", + "draws": 690, + "seed": 172, + "state": 1060026118 + } + }, + { + "id": "exceptionRoutes--classObfuscation--173", + "profile": "classObfuscation", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--classObfuscation--173.js", + "generationRecordSha256": "de760de2b3bd061aeb12fd612e63ff53a9f5c7627d9630c80c6d14a1824d3a30", + "rng": { + "algorithm": "mulberry32", + "draws": 1218, + "seed": 173, + "state": 1759133783 + } + }, + { + "id": "exceptionRoutes--classObfuscation--174", + "profile": "classObfuscation", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--classObfuscation--174.js", + "generationRecordSha256": "a4ded9b594dff2930a57da49168d3b1d528c47b237a4c04c9707a51f303b5b51", + "rng": { + "algorithm": "mulberry32", + "draws": 700, + "seed": 174, + "state": 2195815066 + } + }, + { + "id": "exceptionRoutes--minifyClassObfuscation--172", + "profile": "minifyClassObfuscation", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--minifyClassObfuscation--172.js", + "generationRecordSha256": "fb1ef8e441ee4d926aee86907e050acde0ff4bcd227d93839e06eb1ce686a936", + "rng": { + "algorithm": "mulberry32", + "draws": 690, + "seed": 172, + "state": 1060026118 + } + }, + { + "id": "exceptionRoutes--minifyClassObfuscation--173", + "profile": "minifyClassObfuscation", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--minifyClassObfuscation--173.js", + "generationRecordSha256": "81427cbad9cc2c9e8e29e88dd1841aeefdbd8a575a4c451fdd583fd3ec90cb76", + "rng": { + "algorithm": "mulberry32", + "draws": 1218, + "seed": 173, + "state": 1759133783 + } + }, + { + "id": "exceptionRoutes--minifyClassObfuscation--174", + "profile": "minifyClassObfuscation", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "source.js", + "encoded": "exceptionRoutes--minifyClassObfuscation--174.js", + "generationRecordSha256": "439ced21d3e72f00435f260ca9af73214bf2f7377bc78e3da48628ac1a4c99c5", + "rng": { + "algorithm": "mulberry32", + "draws": 700, + "seed": 174, + "state": 2195815066 + } + } + ], + "generationRecordBoundary": "Generation-record digests identify supplied value data after JSON normalization; retained raw journals remain external to this package.", + "expected": "expected.json", + "expectedBoundary": "Source-derived eventual native trace only; not observed by these parser tests." +} diff --git a/test/vm/jsconfuser-vm/fixtures/exception-region/source.js b/test/vm/jsconfuser-vm/fixtures/exception-region/source.js new file mode 100644 index 00000000..7b4d094e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/exception-region/source.js @@ -0,0 +1 @@ +const trace = []; function make() { let count = 0; return function run(mode) { try { trace.push("try:" + mode); if (mode === 0) JSON.parse("{"); if (mode === 1) { const value = null; value.missing; } if (mode === 2) throw new Error("explicit"); return ++count; } catch (error) { trace.push("catch:" + error.name); return ++count; } finally { trace.push("finally:" + mode); } }; } const run = make(); window.TEST_OUTPUT = [run(0), run(1), run(2), run(3), trace]; diff --git a/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/generation.json b/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/generation.json new file mode 100644 index 00000000..7af6db76 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/generation.json @@ -0,0 +1,78 @@ +{ + "note": "vm-only.js is the VM encoder's output for source.js; each outer-*.js is the outer encoder's output for vm-only.js. Encoder randomness was replaced by the seeded generator below.", + "vmEncoder": { + "revision": "20c5b96bf57337c568579352758d7685358650de", + "version": "0.1.5", + "options": { + "target": "browser", + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "verbose": false, + "profile": false + } + }, + "outerEncoder": { + "revision": "31c5a47a79f97e4b4c2d4b2a8552c11a8b548fb0", + "version": "2.1.3", + "options": { + "preset": false, + "target": "browser", + "compact": true, + "hexadecimalNumbers": false, + "minify": false, + "renameVariables": true, + "renameGlobals": true, + "renameLabels": true, + "identifierGenerator": "mangled", + "controlFlowFlattening": true, + "stringConcealing": true, + "globalConcealing": false, + "stringEncoding": false, + "stringSplitting": false, + "duplicateLiteralsRemoval": false, + "dispatcher": false, + "rgf": false, + "objectExtraction": false, + "flatten": false, + "deadCode": false, + "calculator": false, + "lock": false, + "movedDeclarations": false, + "opaquePredicates": false, + "variableMasking": false, + "verbose": false, + "debugComments": false, + "preserveFunctionLength": false, + "astScrambler": false, + "pack": false, + "sourceMap": false + } + }, + "outerWithoutStringConcealing": "outer-complete options with stringConcealing false", + "randomness": { + "algorithm": "xorshift32", + "seed": 324508639, + "policy": "reset before each encoder run" + }, + "toolchain": { + "node": "v26.10.0", + "decoderBabelParser": "8.0.4", + "decoderIsolatedVm": "7.0.1", + "vmBabelParser": "7.29.8", + "outerBabelParser": "7.29.7" + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/outer-complete.js b/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/outer-complete.js new file mode 100644 index 00000000..9f29de5d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/outer-complete.js @@ -0,0 +1 @@ +function cy(cy){var cz=(cy|0)^2654435769,cA=608135816|0,cB=1779033703|0;for(var cC=0;cC<19;cC++){cz=cz+(cA<<7^cA>>>3)+cB|0;cz=cz^cz>>>15|0;cz=cz+(cz<<11)|0;cA=cA^(cz<<4)+(cz>>>9)+cB|0;cA=cA+(cA<<6)|0;cA=cA^cA>>>13|0;cB=cB+2135587861|0}cz=cz^cA|0;cz=cz+(cz<<3)|0;cz=cz^cz>>>11|0;cz=cz+(cz<<15)|0;cA=cA^cA>>>13|0;cA=cA+(cA<<7)|0;cA=cA^cA>>>17|0;return(cz>>>0)*1048576+(cA>>>12)}var cz=[656,-487,-892,589,-665,331,59,-609,-531,972,12,-831,402,-339,105,-343,-613,-789,184,-60,503,433,630,-745,799,-485,628,-696,127,-133,845,-331,916,744,926,-179,877,-898,-199,887,-835,-779,-879,846,202,-626,-103,525,281,-381,-16,991,-98,-86,904,271,-911,-736,363,447,982,-610,896,-925,921,-608,-399,14,-255,584,296,-528,28,-737,881,237,91,-843,-126,-684,811,-485,46,643,627,-661,-229,-399,20,-457,-523,-542,756,-217,683,-790,692,994,886,448,-627,847],cA="|tay9&(vL&L_@*7-':_RrBwN(\"!3l(Reqh'34^ylgCP4yvNiQurzHR?V( Y|M&]>7Hm\"lh'(+B@bL[y%[ODPLh!sdV+60iNb=6hx]h~[9o9dEpZaAsER:W+<'~+|8n@PA'!qoB%aG5GiQK?r>R:8`>!!puY^d`\\iZ#d!KB.mevFd-xA6K0Cw*&?j]2@=l4(X\\K=7sKuNI?rTIX2ovUm,&lD>s^dLnE*f8R<-h[+EQp>LY0N[YCBA?78jgq=#f9!|Y&^8s'dyH@pA%~OB0Y%_cDr8rYZU^.n$y!n2sL\"4DZyD*?[I(D4`Ze~l\\@ok36Fe/1JM.F(^Ho{^9Z,+i4O[lKe6V+XBPY@T$h}QmE4[+qbn4Y&uCQK+m';5Yqg]3aC,BD1o*[AD?xhrjDjl2'`<|R(^|MlCT=nT?l0Mt ObrJ=Tp'`&H_v(acA#`zk)'bT\\2+hTROh!*u7b+DZ NE=N(7V:ez08&rimS3;c(7~Xwt:7XPuh~p8ZDu2/l\\->d>Ib|) 2X6mTh2M;5q{b%sN3oAE(c:_ivlG<2 lS+T)1u]\\!j]:9+ji&D73A} [{T5i`5_#A;|.lAtk2M^:yglcj7;z@O<^eME<^!*)ZJv@|=J$';Z`$d[*Lr+!w.n]7,Bbu(!j%0,zlp>|;Ic7E>_pG?{gEbt>+q??( uVlKtS9,%`szE+VD&)TrVSo{viZg*Q/eSt%R`8euw=@mC=6}~g)EzCn^\\~F *W5^U]Yz:CwofZv8*ZK3Qc;!\"WTRju()WGPTy2h^RD1I3=_bRhJ7ftf-zA6 8qoVV\"TML!zEH`:8jy?T9{Vi{c9xj|W_^tzlVqp 0z;H:c=3A:, y)fS/P,N6BTt'\"w'878lAeT[B)IaE @%v8fM*5CYpFBkjkS~GmoirBms\"D[vP~lH},{OuP@+S8e[?tKNbJ]\\i+Z%H=6Mr;:%>>JOMU(=Eh~K|C]X6qZg/.5{rcv*#m=#%I#0Q\")J/O/RF,kMu$A?YS\"7@9ko=%d1.E`:y_{vwg&G34d!g\")nLBA}UEy%Ux{'V{E9*n%9fiX4v0^9JxJ3jAz>74)g|P6WZ1EAR'9Ro8Z6^[dk|>y!lKh8a(}9>2U#[ fkmI<'MHwlv%={'jab7YWxHl),vE=\\Eo2MXk`\"HA\"N_?f),LB'b6pgnt:\\VqC]SytLmVw}V?dOxe2V:+dTp(+H5(A&)%,P 1aT~|P/F'v&Uz~-rK0!+&&'noc\"m>a2K},R/w23p|6;hqAVf8U7U?Qv0`zpL_n|W4%)cx^0\\mM$]>]|fzoC6Ql[mDD(:o;|>cy|szizXZ|G( NXGdg#-gZw,NK94mb{WS3fYB18]P0^:c@iIB7.0;B83Iyt_M6MOW|<2?*I#(GPT'i)n340Zv&aGRxs.TGDO:+;b``g,Is%#@|5FNP/}R9,/$b,p= \"5B>7uhy(*TJ?k;$)wS2 !Qpdy{ju@7c\\5##hqa&G'N/1U uJ^Uks+m8a=]V%bbEbaIE)ebAlNIw84!a7xsGU~,^4t-Qpk<!P\\`(/,Z,vni1D3sFZ)eFRXSiN/sjQJ?$\\%n^jUhlGV!1kxK8w0U0b74sEej.}4K(Umnn?7?juWClju+&7xP655`9_<]ixms(u_%>aCUMs9?z`WYd?S@# >CL``3>]4C3E(gdeSp[et8 `%)gxAL_&zYI6YA5jL(.{*K0R[,=Hz@4QIO;ZqD#H#q;&H=cR%JJfJ6J`w@;.s@^n^w0abHr4-Dh5.~\"+{Fx 9ABV{Yz7/\\B3u06+@0C)My=82{C|EvfGpn76RAx;MeJA\"&\"i3PGVIi@RI2}{tJmaU\\\\#tJlwcv2aui&N4RDPA_;QO;7UshZZ@ByS[tt\\!OFkNmBs~dxS.##V]R*wPM wvG[vD_U#cOTuky@sOjGO)7+3\"D[B3\\I\\!P1=c +_kguvEr'}ks3v5lMS>C(9 Q3%f8}tB31Cr.y@$va5E>,c(AiJTuj-1MD{O}*&twJ3\\?G:O''9m`rco$F.W}$:FDJ[lU|&h*`qm|yDU^0%\"x#/)a(-Ewxjb-U$,o.^eFxLhFyX8D\"@EN0g}lQ^0+*_2!;v\\yNJ7'L.WMJ2*Vu]~gy8k6f'\\#n$:mWxD>Qesd:e~}!;v8B:@FnFsiP-/pQB*9|,O7u0|o&l^{F`*0syFxJb5Q-0@!WpNL2UM}kH@P3PsH\"]{W.I]Q,ke|fQeKS6qQi<}~IzWYcJ 7~?u,'Eo2^-+[IB%TYBT]4l@t|/y]\\CGf#~12h)TtA`WS:Te:{5wa:2f]S!F,],jlW@b)6a hYk%|nOe`j$P%.Va$qN5rE6NifE')ym :4Q`T|,&O!^ @am\"]lMkk}$\"a%ez}IR2aCxphWEjl{g2/K;U{[)ZNkbapdZIw,>~&5sx\\GSbw(RmfR%DC+Z!-NW6Jjw8NQt:!BNANYq+5Z'}QM)l]$ZETU7@M7)M~JM$93_@zyTXGc]UI75=q65>Y6#Dc1PB8.^0mqk~CgxOO\"6@]\"Fr6J3G0C{Xe[JQDCJ3;rTp!n6=q%H};\\ Q8*On$2jtw4G$eTSw=nQ|Ou ofjlm cFZ'H\"N3~$?VbpM5T \\_&,(2$B8S~@&v4GH\\.rsf9}u@d`xcHD2H?lpYc*p@s\\II].j>RI9p~$C}E88lAROz09>I}b\"_gs~ZMUF$T/:k)zkC7BahV>L|6&yMXHF<,dwmRp27$j5P6=[KKFfFs8]\"\"y{iq-] vJi?=[kEJDyjnj[F@H8Z%,.+~Eiq:9O1KRBk+bh@;4S\\w05ww)jcmA d_>gC2>U&iN]'L~-&DmJ}QE~n~`,V{jm^!V`6/=L2eAouIYZ#/-:u#Q]bxX[%z<31j!#a?!YnNh90^q\\W)kB^@=-{3@XHmDa9wqM_V:+5#(@BJp@[+Q(Bz.?-p5Q1q}LH2Q)==*f3z*I,] O_q,\"W~k9UzJSzg g8OLVQqUA*->{'?-v3E8+,[%\"NMCqRj>^Ws~K:@aRV1v@6Wj :VPq$34t0E$/Ag'{.FK4G[)VMp?]rC;iuB!)cYXe4K e7p\\1a&Sm3_4%oNeB17%zXm5a=Bx#`Z([#QMbZRPArg|f)u^IVn'w-yaso[wP+ iPe+!AYIiT]hwBIGJZZKWTb8djh~,zq-s~gB3\\y*3v:kGi$nqA: _%u9.]M~_Aw~~ZV'.G}E,.Zrc2Lh2_Co@Bl'Cznn6lsZKj?A W`%kg>(f~}~G7/Zb][ YN6mva. H13XlgH,$f='R_k<0`>9I3.t me\"LVVWMz/~&UceR)i=jFj9(fOx-~ik*g-vtjT:^gj2YY,$bwf9qhHhC3!evFpJtG4 |jW*cd~9QXzH7ApQ9xmd`}co1btY&j#~R35IwrC%`x?IH0IS5^Z%+[^NeC1faVLbFBcY^HNi4?G{OUw^p,GJs?Lam1^-kHyXY0`fn{0`7@4bR!#d8imq3#-D)n~S/A\"p6%wp6WrSziO3NmFiZd>D<.0bngVKk2\\ejKJ}z$c,,P+\\V?6GxIr;Yex7xq:J!aTRp,E)\\G -312`u>NzZmt#0~[TgteaISgo9N/[-XA\"nVgCCoPE21|RX|B-i$\"WtTE?^ZSn@k=MkH%rF(vk14M!Z1I^+Um:2sIU, >'UqIl-n>9,2DFO9=>iPyU:BN[K!aiJ\"p9HiIf>)s-yiHAOZ)e,Aq84Fh8smN/ u=+Aa(I1.`QHbg`ShffEd#t2ZHXe3za;xY`@yCc}MO0Tb)Z:3B1//t?ZlA5)]s2\"PSs\"R7PhVe*]=?;49Vqz)D|b`g?|#mg8xB(h*fw}C6(Q(9[t#BuCR(:q >$x4y&1>kgsW]Wei\\D.y5gb*\\C^lHn`- & ||hm>(Jt3JLj$qTeb;&eS^Y'+G@W<,,=#dOj)(gm'&hMNRO|JnHyQ@m$BxhzI[]HfzKG{$0,^Qd>!c>9{D0i!!~Z3f\"0bI)PNj.\"V%1-=\\dEaRZ^SDaRpRJk'DL^K*GDz=YDt~eXqP]3z/op3[g}euV(m(jq UX@gOee-cG$@c{QMH`bQ'*Uv'0z38qMt:.*_RioekEbSHbA1=lM!_`GFjYTd/,sl%)!I>u2GlFA(8<\\Hu|Tj1xsiR=w` NwakPT{f:UAFvX}5f%^k=&?F\"id3LaT]$G~BExHR[J* ~MkHX=sk?}+Lok8iv17?MrjYeV1uTTnuhxV/yl}zkFOn-Q)w)V:/=1S]rdClFDv\\{C1wi%@\\n\\C>oy\"'X|ae_|SK\"j95I+ yISXs'{U ID]i8~m1HrV!To>}'1=8\")=!?` >~nSq2R*HvnW2zYqV':}+2okTW@(ZV 72=p$>;mdF.%11J,?KO:5Kfl%DysyR+\\;5:\"'<83MD^8X^xWn<17-%}W02Bk%4X$DY}CLvbP*Wh`m+WY\\X.L*`VM7j};LK/&1iGyyf4X?/7#=5{5`^{&lhhl=KvHKaNg7cr:lNzc?9*D9YI9+v+ADxfJ*5!\\/Dh^1z`0&['AxqWT@\\;5|Rdm3-nS#$*[J9 .=t:o@G%^x.T;tn5yS%bP4gd]2t6U~L2V&KpS(H?-92_wOYSki.f I0GJK/LbCOF5MGmBibpR![$X4A0Fh~-/+]IEiLv>5-sGPT5UWn?KDUr`7u&Zjf33|jk0t@|tQ\\2-fpVq!F|m;*IQ^eXhdKz?t*qEYlojue0ee6>w)]0ynR^,{F=gML!Ri|~op{Sa\"GhN5Aw&D>%:qk^Z }M,`l-M+DeNsz.ydL.r`cEMy8.K{*Lx;++dB.oMa5Z'lZDQ6t)Crs9VVy2gqBu/!`:yDw%c@n?$AC/Q)hNjSZ+Ei,5@d65D\\@|`i11ymu?`__ec[?|}JO~!j?.uX?BntA AArs$RFkg%M9^7}zj t#HN2=k%dm>l}X/H+d{:{Sn^YcA!PrpM7YF@m`GfGY&|1zA5%.DI13~u6|V!Cj{uo,v!i0}3l#oKHCi}n~b; _5x\\Y_?NA^ItV^UUm:f86?1gDU$Rz)iEj3hhZcOXf2Nhemnuy(H2}/'ZOIndBx68_o7tz9MO,N8Z\")g_GdAYi]wo8I Ubk^U1Z%6f?Q[Ymkzbro)(AV1.h\\/&WcO*Fg3S\\e/xwmbPHY:k].x\"zXFHxpXkU,C`2q9;y6 <)6>(@Ab5|9)}l'9X'Rxd=u8}0#NP/oF@fV9m37;ElSL3f)MK;o\\8_srU@f_6icq2ftK,yC__.S*}8tzM.Q@_JngjlH,uDo+dGWoYiG+j1P1\";=\\p.? /[aM>oG_#MO=9PX\"*H070*F|f 5q^u9lwX)0L)S#P0qL?24LyJW*U`g@#KGFXwnaENvDx+0xeS(~Sy!Uk.l{m+Al*HpX$}m_i|enu315:fwAAZ(+9-jE-`*N$j)$J|d[cIrh%1*lpT_}\"MctmF*]@%M*=u\"Yt|rVx1mjNMW>b}'{RfHafct`B pM1#QZ|Y)qpF.CBz[:@LHv_7N3(2/Czl7np{l\\S&zxh[eFz|@akTBQ(BwdG#F]3G[~2#}DnQ(RnJ]?amtupeg%oJo+VEEhq[J25C*z?R|tx{3|Cy WRLS@ g\\)S{O&H\"y\\^bk6\"qU[wIq-u%~?afP'A^^P4BNq~ff#6MmP|D<-(t5YIIKJi=Z.^_ZTrMGcP5_\\3PCC[BVmP+osDOw},R+q+LN_7y;h1\\49qYP:ncFKjn,\"%:p)-3PT!u/chz:YkK_B5'6bRs$XLs=6/@@)qoBJ?l9`!pJQ(cojUFAqjvC&/]\"G+{)a'QVmik^LD*[!qlG=m104/K7b8v3@UU E6Q>e;X?_ !JcNE`.NDrnhYjrNe8Fbo_K#5Xu07Tb_n|k%*>QL%Vr/xv2~eTgU ~NurB8f:G?!ecFqi^S)2HJ yT_;oCJ{ugW]V09pFH \"QhpiD/~e6bf:Q,D]PWMmy$=,?2rw5T{SKn1{v6{lw$}'V@`-N9CSszOXh'lC[j<~lfHQQ!l/lKI`R-$|[T!4[afg_^yY:JWoR{&]vd:P0_C!T*$~D[yXsn^e)RWP3=F;A|1]?r&E*6=zx3kZ\\63Z6bsWZqH9^AQJr@f9J1pa\\w)bfq1PtM}ThnnEJ*Eo/,,7un)cC_tMpI&6M{-$sHU9mC:e*>WElenS}\"q]q+lrw>]AwJig>9j/qXt-&W~!TanL.yt1;FhUkpUVG=VX\\uBu(yn*9yc076%}Tx%3FEsAAa >0d2+,:98vV3]_s)?jG0y]h,k]TpE`}y}[,?|lt%\"_G91>ETMTJ-cJ_]%B'*6$yj__.(wt_Nm4F}+IIaHn*q4a\\=SSt/*'KwLtz[)*h+7t&d+m[8l:6-=8>>lzqA@zN-ldm]v=j4n2'.JA!ZeRYpl*AH;`3H~8|CndZl-oro;:P-k%7,zp-dBqh%o-%^{Wyl gB5wk[(9U*3WeECBD?3o'A&+>-MXT8A#F^4sWx>Re{#c[/5{8>k68GCVF|uAxja-_zImu3;[oFD,L@E3I?><{`\"R-x(P1z.'a6-wZ1|~NQ2bIYDMq+uOX0_v^d@MM\"M=[mwFO\\1D XqPkr,Bb]$*Wyvz<+HVl$oMOy%)xCvCxGg~\\;0zb'o_.sS$+jnT2K0~80%_`evJWLxS[)`@-~aqM*]9n<$~?As%Pj[ _wsK_RoQ!)5}8a>QiVt.nZ~7qV0ogk}jm]ob>T8b(@i8Pv^O%uTh4gVBga*Ql?ZHya5|F,W&K|i`Uv)>3qs#97~t5X cGk Ab-Ycnwo~S\\-@9 tZLcifrhv}~Z(IBA\\!rv0~jhx^?FJ3J|,'9xcyp#@O8G#JG?3#sZJ~@UP2B~a M [YL[~;{x_Eweo.8o^uJ$jaO1[@q`KnR\\32+'GmxbUcBi}p\";.Z[:f_gu8adaN9KT0S,2vB6ZS.\\&[oLBgXp~\"4'Ft(90,-C~Ei,$ZHV\"4O8.'BD1/2,K79sZw]blR|w-fjIKrHt0EN0.+RnZ]8H];Lw:rf@Q1A,s;jENJgC/r> Q;oIFAy;I?rAB`,QbI?9-[7/s~$&HmrBGoD+W3CYSoa,,qknn\\S\\8SuF%p,HYQ&nh\\G\\!ZAHQ5OWY_!i4jfx3`e(M.DFss+8D/sO^PY84+hjeYBCO4J$6]Dgcp^*q 8o/FBb9);TnAp[V)>S$mA|-\\2;6xv@1f.F+!x1d~{(!uaLs{Mx>ik3c4.Z <\\ZLrq#-Pfn>Wn:?<8?UU@VG+rE.`CjqxBqJRC1dI9(kS@y\"J0Uek/)GUYj}F Vc~-~N}wk\\=Oqv?2uHau(u0U}'pG~YFF1Oyq<&-bHqL+Bf!RG*;n}}x@w U*!9ra&vv\\{L(TcO?2G9;6Dv/<5!CT IK[:4{-z/jWVLv=3DVWoPy5{u[xWClGv ep(`o5N3<=%S=[YKqlr(7o2..<3)QVEHG}%3IDc){BgMT6Co+tUo5lgT5;ifQ26*}ixMc_`FpYuC6OA~tzgYj(yKi>@X\"zVf.gWMqoVy`ayvyw.7D}jI7B,BshaX+8L]hh q8SM|)|&ac%CnS\\#$8.d6&.*bYqm2xy{`*(,Dvm-s@|U>m}U$CNI`s\"|1UD87e6iU{@Zk?tAIlwb.3~WbwX=)gxUN~LBJZw7,Si6(<&wnRqYLZVADY8tZPUlKK~H!XJ>IoVi\"q.f;^#}r6Zf4_9@1HI|jWlUuxB9vx`NeEbyf=GQw398_:.2F(lKY}^tt\\ :KzG,52^IOBG$_!Fzkn?fs5_09)i0x3,>o&Og2\\wGw96R4L=>K(r'm`IYyZqWb\\5$*nlltz=-.$<<#40\\^h7=TIM^So63:5U|\\sjYz`xJ3.rez\\brr03W_R;!_4usACh`z+,Cp;2?jJ:_b_*UA\\JYBU6`&sZe \\M)p67AvoE\\1ie(SS|`fI9i>{)kW+x{\"e(UImqDuLD{T&gj9tW^-Em$)AZ*kr`9$]gO2uKxa0<*Qj{\"}w;`T@[CEReE^QKs6,inm3DZ75WdQbnI)otTiYTMdVg<%Kpbt&Ylwzu|htXo)H)5~9Eh9Ex4.PNs6pOV+gvjd6\\dWg_0aPAp)dDWEv9nv^2qeU7`#:%}5G2?lc_MQg|3gtx/~9^sQ3?ZBU[fKN:O2':1zINnl*LaP5BJ` s,YL|aViW8o,11/'|:lg$63_LLge!iOwz0eK*v2( Z5;(02\":n>~ItYr\"nv05t#[+PH@?^bfr3kMHB4yvR+U@Yeed6iQ;6@Zrh[8NTCD2O;%Sc#{$//f~v#RSno8xRDJ09L*D4jiGLH6R\"l[\"xWN^cEmp,{KfJ&HSC;,9Mqn$*aqwj}oMNpx^rs0=Ub6 (4eT`E+v]v-]jr*'`$Hgb'\\~Ku@fg5nb\"r7`j K'u]@e3x>>13)%95+95)%95;var cF=cA["charCodeAt"](cz+cD)-32;var cG=((cF-cE)%95+95)%95;cC+=String.fromCharCode(cG+32)}return cC}function cC(cy){for(var cz=0,cA=0;cAcy[31]+1303){cy[2]+=cy[10]- -1291,cy[16]+=cy[75]-1203,cy[28]+=cy[40]- -2019,cy[46]+=cy[5]-224,cy[48]+=cy[74]- -2683,cy[50]+=cy[75]-1334,cy[54]+=cy[0]-800,cy[63]+=cy[10]- -415,cy[67]+=cy[76]-857,cy[76]+=cy[44]-4923;break}case cy[7]- -1468:case 199:return undefined;case-336:cz["cg"]["D"][(1,cz["cg"]["b"])(5090,cy[25]+494)]=typeof exports!==(1,cz["ct"]["b"])(5103,cy[56]+922)?exports:undefined;cy[2]+=cy[49]- -1780,cy[16]+=cy[67]- -193,cy[28]+=cy[70]-211,cy[46]+=cy[35]- -330,cy[48]+=cy[58]- -119,cy[50]+=cy[30]-9,cy[54]+=cy[22]- -472,cy[63]+=cy[19]- -877,cy[67]+=cy[50]-1742,cy[76]+=cy[22]-4504;break;case 33:case 171:cz["cg"]["D"]=globalThis;if(typeof window!==(1,cz["cg"]["b"])(cy[28]+5409,cy[34]+-915)){cy[2]+=cy[14]- -198,cy[16]+=cy[13]- -372,cy[28]+=cy[56]-2237,cy[46]+=cy[43]-485,cy[48]+=cy[43]- -477,cy[50]+=cy[41]- -1929,cy[54]+=cy[50]-285,cy[63]+=cy[56]-19,cy[67]+=cy[55]- -1323,cy[76]+=cy[73]- -907;break}else{cy[2]+=cy[46]- -1131,cy[16]+=cy[38]-681,cy[28]+=cy[69]-1861,cy[46]+=cy[34]-534,cy[48]+=cy[6]- -543,cy[50]+=cy[55]- -1065,cy[54]+=cy[47]-1557,cy[63]+=cy[53]- -75,cy[67]+=cy[23]- -1970,cy[76]+=cy[29]-417;break}case cy[19]- -326:case 415:case-274:[cz["cu"]["b"]]=cH;cz["cu"]["a"]=function(...cy){return cE([...cD(0,2),-757,...cD(3,16),834,...cD(17,28),-800,...cD(29,46),340,525,895,-381,181,...cD(51,54),-2853,...cD(55,63),834,...cD(64,67),191,...cD(68,76),-901,...cD(77,79)],{["cu"]:cz["cu"],["ct"]:cz["ct"],["cg"]:cz["cg"],["cv"]:{}},cA,cy)};cz["cu"]["c"]=undefined;cy[2]+=cy[28]- -480,cy[16]+=cy[3]- -1299,cy[28]+=cy[77]- -622,cy[46]+=cy[47]-71,cy[48]+=cy[47]- -42,cy[50]+=cy[25]-323,cy[54]+=cy[40]- -692,cy[63]+=cy[40]-1804,cy[67]+=cy[45]- -1310,cy[76]+=cy[75]-466;break;case 78:cz["cu"]["d"]=(cy[18]+-183,cz["cu"]["a"])([...cD(0,cy[56]+929),-264,...cD(cy[63]+2673,cy[62]+-870),-(cy[33]+-25),...cD(27,cy[47]+-496),cy[75]+-3,cy[9]+-127,387,358,...cD(cy[11]+864,cy[11]+898),-(cy[31]+1213),...cD(68,cy[65]+678),-371,-(cy[58]+165),675,...cD(cy[34]+-853,77),-398,...cD(cy[39]+-809,cy[45]+716),cy[60]+-726,...cD(cy[75]+-146,97),-(cy[51]+-259),cy[74]+5]);if(cz["cu"]["c"]){cy[2]+=cy[75]- -410,cy[16]+=cy[48]-1987,cy[28]+=cy[76]-72,cy[46]+=cy[71]-290,cy[48]+=cy[46]-329,cy[50]+=cy[23]- -1585,cy[54]+=cy[41]- -691,cy[63]+=cy[9]- -1193,cy[67]+=cy[26]-1249,cy[76]+=cy[24]-1272;break}else{cy[2]+=cy[58]-748,cy[16]+=cy[42]- -651,cy[28]+=cy[7]- -591,cy[46]+=cy[25]- -221,cy[48]+=cy[7]-298,cy[50]+=cy[70]-1127,cy[54]+=cy[42]- -1366,cy[63]+=cy[18]- -1642,cy[67]+=cy[68]- -905,cy[76]+=cy[65]-351;break}case-823:case 837:case-400:return cz["cm"]["d"];if(cy[42]>-(cy[45]+1505)){cy[2]+=cy[5]-781,cy[16]+=cy[62]- -1406,cy[28]+=cy[7]-81,cy[46]+=cy[39]-1316,cy[48]+=cy[39]-1282,cy[50]+=cy[4]- -1538,cy[54]+=cy[11]- -1876,cy[63]+=cy[58]-547,cy[67]+=cy[7]- -293,cy[76]+=cy[57]-134;break}cy[2]+=cy[35]-1124,cy[16]+=cy[40]- -6664,cy[28]+=cy[17]- -363,cy[46]+=cy[0]-720,cy[48]+=cy[10]-1384,cy[50]+=cy[3]-1330,cy[54]+=cy[75]- -138,cy[63]+=cy[68]-51,cy[67]+=cy[38]- -877,cy[76]+=cy[17]-150;break;case 968:case-948:case 668:cz["cg"]["o"]=cz["cg"]["l"][cB(cy[cy[49]+442]+233006,cy[26]+13224,6)];cz["cg"]["p"]=cz["cg"]["l"][cB(cy[cy[69]+-572]+117480,cy[68]+14117,6)]||String;cz["cg"]["q"]=cz["cg"]["l"][cB(cy[51]+101148,13872,5)]||Array;cy[2]+=cy[70]-274,cy[16]+=cy[64]-1174,cy[28]+=cy[25]- -322,cy[46]+=cy[10]-160,cy[48]+=cy[59]-719,cy[50]+=cy[70]-934,cy[54]+=cy[37]- -1353,cy[63]+=cy[15]- -767,cy[67]+=cy[78]-593,cy[76]+=cy[26]- -632;break;case 63:cy[2]+=cy[13]- -789,cy[16]+=cy[57]-1566,cy[28]+=cy[26]- -62,cy[46]+=cy[31]- -760,cy[48]+=cy[6]- -336,cy[50]+=cy[16]- -5069,cy[54]+=cy[6]-1104,cy[63]+=cy[69]-400,cy[67]+=cy[10]- -304,cy[76]+=cy[57]- -1606;break;case cy[24]-281:cy[2]+=cy[31]- -904,cy[16]+=cy[38]-599,cy[28]+=cy[75]-1121,cy[46]+=cy[54]-1224,cy[48]+=cy[45]- -832,cy[50]+=cy[46]- -706,cy[54]+=cy[38]-6240,cy[63]+=cy[44]-461,cy[67]+=cy[66]- -556,cy[76]+=cy[11]- -7731;break;case 784:case-385:cz["cg"]["g"][(1,cz["cg"]["b"])(1203,11)][(1,cz["cg"]["b"])(1278,cy[7]+617)]=function(cy){function cA(cy){function cA(cA,cE={["E"]:{}},cI){while(cC(cA)!==-750){switch(cC(cA)){case-969:cA[13]+=cA[5]-2210,cA[18]+=cA[22]-827,cA[30]+=cA[27]- -6090,cA[39]+=cA[32]-1480,cA[43]+=cA[91]- -1218,cA[51]+=cA[9]-4464,cA[59]+=cA[39]- -839,cA[68]+=cA[79]- -853,cA[74]+=cA[51]-82,cA[89]+=cA[90]- -1418;break;case-339:case-930:return cH=true,(1,cz["cg"]["d"])(cE["E"]["d"]);cA[13]+=cA[52]-1676,cA[18]+=cA[43]- -6299,cA[30]+=cA[49]-934,cA[39]+=cA[31]-669,cA[43]+=cA[64]-2193,cA[51]+=cA[45]- -547,cA[59]+=cA[13]-687,cA[68]+=cA[64]-2573,cA[74]+=cA[88]- -460,cA[89]+=cA[86]- -958;break;case 973:case cA[90]-371:case-673:cE["E"]["f"]=cA[26]+-628;cE["E"]["g"]=-1;for(cE["E"]["h"]=cA[1]+487;cE["E"]["h"]cA[85]+749?13:14;do{cE["E"]["d"].push(cE["E"]["e"]&cA[25]+740);cE["E"]["e"]>>=8;cE["E"]["f"]-=cA[21]+-425}while(cE["E"]["f"]>cA[20]+-496);cE["E"]["g"]=-(cA[4]+666)}}if(cE["E"]["g"]>-1){cA[13]+=cA[24]-495,cA[18]+=cA[65]- -549,cA[30]+=cA[53]-5222,cA[39]+=cA[25]- -415,cA[43]+=cA[27]- -887,cA[51]+=cA[76]- -221,cA[59]+=cA[0]-972,cA[68]+=cA[81]- -5572,cA[74]+=cA[27]- -182,cA[89]+=cA[43]-117;break}else{cA[13]+=cA[55]- -1520,cA[18]+=cA[23]- -479,cA[30]+=cA[87]-3557,cA[39]+=cA[16]- -1622,cA[43]+=cA[37]- -1118,cA[51]+=cA[49]- -1357,cA[59]+=cA[45]- -895,cA[68]+=cA[92]-625,cA[74]+=cA[33]- -72,cA[89]+=cA[73]- -302;break}if(cA[cA[64]+-904]!=-(cA[77]+1632)){cA[13]+=cA[90]- -2410,cA[18]+=cA[90]- -794,cA[30]+=cA[42]-3702,cA[39]+=cA[3]-190,cA[43]+=cA[85]- -744,cA[51]+=cA[77]- -3743,cA[59]+=cA[78]- -588,cA[68]+=cA[44]-161,cA[74]+=cA[11]- -384,cA[89]+=cA[16]- -781;break}case cA[42]- -344:if(!(cA[cA[2]+938]==-103)){cA[13]+=cA[28]- -213,cA[18]+=cA[21]-320,cA[30]+=cA[39]-2098,cA[39]+=cA[87]-782,cA[43]+=cA[15]-112,cA[51]+=cA[77]- -1252,cA[59]+=cA[80]-796,cA[68]+=cA[72]- -2147,cA[74]+=cA[87]-822,cA[89]+=cA[10]-139;break}case cA[42]- -308:case-869:cA[13]+=cA[84]- -1182,cA[18]+=cA[44]-370,cA[30]+=cA[50]- -869,cA[39]+=cA[47]-447,cA[43]+=cA[14]- -173,cA[51]+=cA[23]- -3142,cA[59]+=cA[14]-4838,cA[68]+=cA[87]- -463,cA[74]+=cA[36]-1904,cA[89]+=cA[72]- -1281;break;case cA[39]-809:cA[13]+=cA[79]-444,cA[18]+=cA[20]- -3786,cA[30]+=cA[38]-1223,cA[39]+=cA[20]-1574,cA[43]+=cA[70]-1570,cA[51]+=cA[21]-518,cA[59]+=cA[39]-275,cA[68]+=cA[3]-1584,cA[74]+=cA[35]- -2077,cA[89]+=cA[75]- -176;break;case cA[69]-1231:cE["E"]["d"].push((cE["E"]["e"]|cE["E"]["g"]<cA[52]+186?13:cA[70]+-282;do{cE["G"]["d"].push(cE["G"]["e"]&cA[0]+-211);cE["G"]["e"]>>=cA[93]+225;cE["G"]["f"]-=cA[93]+225}while(cE["G"]["f"]>cA[32]+-909);cE["G"]["g"]=-(cA[66]+134)}}if(cE["G"]["g"]>-1){cA[0]+=cA[37]-2849,cA[5]+=cA[11]- -735,cA[26]+=cA[85]- -1132,cA[30]+=cA[94]-1613,cA[37]+=cA[24]-3803,cA[53]+=cA[14]- -107,cA[66]+=cA[77]- -4179,cA[68]+=cA[4]- -609;break}else{cA[0]+=cA[65]- -141,cA[5]+=cA[47]-600,cA[26]+=cA[9]-667,cA[30]+=cA[25]- -378,cA[37]+=cA[64]-1985,cA[53]+=cA[86]- -643,cA[66]+=cA[86]- -781,cA[68]+=cA[70]- -466;break}case 268:case-209:case 838:cA[0]+=cA[23]- -1586,cA[5]+=cA[13]- -134,cA[26]+=cA[71]-765,cA[30]+=cA[75]-240,cA[37]+=cA[43]- -1505,cA[53]+=cA[43]-2584,cA[66]+=cA[19]- -271,cA[68]+=cA[22]-1020;break;case cA[56]- -736:cE["G"]["d"].push((cE["G"]["e"]|cE["G"]["g"]<-(cA[17]+1513))){cA[0]+=cA[32]-663,cA[5]+=cA[70]-275,cA[26]+=cA[95]- -624,cA[30]+=cA[36]-54,cA[37]+=cA[85]- -2601,cA[53]+=cA[40]- -1037,cA[66]+=cA[10]-2796,cA[68]+=cA[60]-164;break}cA[0]+=cA[59]-194,cA[5]+=cA[77]- -864,cA[26]+=cA[37]- -709,cA[30]+=cA[37]- -1698,cA[37]+=cA[45]- -2566,cA[53]+=cA[21]-231,cA[66]+=cA[9]-3756,cA[68]+=cA[78]- -944;break;case-497:case cA[36]- -55:return cH=true,(1,cz["cg"]["d"])(cE["G"]["d"]);if(!(cA[cA[33]+-726]!=cA[92]+-452)){cA[0]+=cA[1]- -113,cA[5]+=cA[94]-403,cA[26]+=cA[20]- -485,cA[30]+=cA[64]-811,cA[37]+=cA[13]-948,cA[53]+=cA[55]- -1053,cA[66]+=cA[4]-98,cA[68]+=cA[69]-956;break}cA[0]+=cA[60]-1253,cA[5]+=cA[69]-428,cA[26]+=cA[89]- -1083,cA[30]+=cA[93]-693,cA[37]+=cA[18]-426,cA[53]+=cA[63]- -2000,cA[66]+=cA[19]- -110,cA[68]+=cA[15]-651;break;case cA[16]- -851:cE["G"]["e"]=cA[26]+-846;cE["G"]["f"]=0;if(cA[cA[86]+289]==943){cA[0]+=cA[38]-2373,cA[5]+=cA[29]-667,cA[26]+=cA[33]-1735,cA[30]+=cA[54]- -671,cA[37]+=cA[2]- -1898,cA[53]+=cA[62]-526,cA[66]+=cA[33]- -266,cA[68]+=cA[60]- -114;break}cA[0]+=cA[27]-1409,cA[5]+=cA[15]-382,cA[26]+=cA[83]-1939,cA[30]+=cA[26]- -2132,cA[37]+=cA[4]- -2735,cA[53]+=cA[32]-960,cA[66]+=cA[40]- -1293,cA[68]+=cA[10]- -322;break;case 533:case cA[83]- -25:case-851:case 470:case 480:case 711:cE["G"]["c"]=cE["G"]["b"].length;cE["G"]["d"]=[];cA[0]+=cA[93]- -3365,cA[5]+=cA[8]- -1310,cA[26]+=cA[72]- -1531,cA[30]+=cA[57]- -596,cA[37]+=cA[38]-2682,cA[53]+=cA[91]- -101,cA[66]+=cA[52]-1218,cA[68]+=cA[38]-751;break;case 588:case cA[10]- -108:case-186:cE["G"]["a"]=cB(cA[34]+165699,1543,91);cE["G"]["b"]=""+(cy||"");if(cA[70]-(cA[27]+794))){cA[1]+=cA[50]-526,cA[12]+=cA[47]-397,cA[22]+=cA[52]- -390,cA[42]+=cA[37]- -1381,cA[54]+=cA[64]-1732,cA[58]+=cA[9]-907,cA[59]+=cA[46]-656,cA[68]+=cA[45]- -764,cA[78]+=cA[39]- -311,cA[80]+=cA[61]-464;break}cA[1]+=cA[8]- -1228,cA[12]+=cA[25]- -1210,cA[22]+=cA[33]-2189,cA[42]+=cA[52]- -809,cA[54]+=cA[31]-587,cA[58]+=cA[61]-50,cA[59]+=cA[74]-1421,cA[68]+=cA[53]- -1505,cA[78]+=cA[28]- -1015,cA[80]+=cA[1]-1176;break;case cA[72]- -76:return cH=true,(1,cz["cg"]["d"])(cE["I"]["d"]);cA[1]+=cA[29]- -698,cA[12]+=cA[6]- -593,cA[22]+=cA[12]-2540,cA[42]+=cA[71]- -344,cA[54]+=cA[64]- -632,cA[58]+=cA[48]-14,cA[59]+=cA[42]- -1937,cA[68]+=cA[22]-231,cA[78]+=cA[13]- -607,cA[80]+=cA[3]-1426;break;case-580:case-723:case-942:cE["I"]["d"].push((cE["I"]["e"]|cE["I"]["g"]<637){cA[1]+=cA[15]- -1537,cA[12]+=cA[59]-883,cA[22]+=cA[32]-243,cA[42]+=cA[45]- -1038,cA[54]+=cA[10]- -1629,cA[58]+=cA[57]- -2005,cA[59]+=cA[32]-877,cA[68]+=cA[39]-566,cA[78]+=cA[72]- -363,cA[80]+=cA[68]-4207;break}cA[1]+=cA[49]- -1010,cA[12]+=cA[25]-298,cA[22]+=cA[12]- -4469,cA[42]+=cA[66]- -995,cA[54]+=cA[78]- -747,cA[58]+=cA[41]- -1781,cA[59]+=cA[13]-1008,cA[68]+=cA[4]- -1094,cA[78]+=cA[33]-621,cA[80]+=cA[20]-3928;break;case 500:return cH=true,(1,cz["cg"]["d"])(cE["I"]["d"]);cA[1]+=cA[32]-213,cA[12]+=cA[42]- -901,cA[22]+=cA[11]- -1592,cA[42]+=cA[29]- -538,cA[54]+=cA[79]- -1946,cA[58]+=cA[72]- -1199,cA[59]+=cA[37]-4669,cA[68]+=cA[20]- -117,cA[78]+=cA[5]- -103,cA[80]+=cA[51]-457;break;case cA[41]- -1741:cA[1]+=cA[20]-975,cA[12]+=cA[53]-148,cA[22]+=cA[43]- -1316,cA[42]+=cA[45]- -941,cA[54]+=cA[28]-1561,cA[58]+=cA[36]-195,cA[59]+=cA[53]-789,cA[68]+=cA[44]- -708,cA[78]+=cA[21]-787,cA[80]+=cA[57]-822;break;case-866:case-199:case 803:cA[1]+=cA[15]-284,cA[12]+=cA[78]-610,cA[22]+=cA[19]-1486,cA[42]+=cA[51]-2455,cA[54]+=cA[64]-1541,cA[58]+=cA[65]-465,cA[59]+=cA[74]- -153,cA[68]+=cA[44]- -210,cA[78]+=cA[24]-2434,cA[80]+=cA[19]- -5356;break;case-1:cA[1]+=cA[9]-1917,cA[12]+=cA[41]- -308,cA[22]+=cA[71]- -2780,cA[42]+=cA[38]-0,cA[54]+=cA[58]-405,cA[58]+=cA[8]- -406,cA[59]+=cA[26]-1452,cA[68]+=cA[40]- -763,cA[78]+=cA[69]-951,cA[80]+=cA[6]- -656;break;case 68:cA[1]+=cA[5]-30,cA[12]+=cA[21]- -73,cA[22]+=cA[18]-1028,cA[42]+=cA[46]- -447,cA[54]+=cA[17]- -1312,cA[58]+=cA[37]- -30,cA[59]+=cA[0]- -583,cA[68]+=cA[21]-700,cA[78]+=cA[63]- -1609,cA[80]+=cA[6]-845;break;case-83:case cA[71]- -1428:cE["I"]["f"]=0;cE["I"]["g"]=-(cA[51]+-990);for(cE["I"]["h"]=0;cE["I"]["h"]88?13:14;do{cE["I"]["d"].push(cE["I"]["e"]&cA[8]+786);cE["I"]["e"]>>=8;cE["I"]["f"]-=8}while(cE["I"]["f"]>7);cE["I"]["g"]=-(cA[24]+-798)}}if(cE["I"]["g"]>-1){cA[1]+=cA[70]-2162,cA[12]+=cA[11]- -600,cA[22]+=cA[3]-398,cA[42]+=cA[65]-1084,cA[54]+=cA[75]-750,cA[58]+=cA[23]- -397,cA[59]+=cA[25]- -1300,cA[68]+=cA[16]-256,cA[78]+=cA[9]-2551,cA[80]+=cA[76]- -4378;break}else{cA[1]+=cA[37]-339,cA[12]+=cA[45]-388,cA[22]+=cA[7]- -4315,cA[42]+=cA[78]-2016,cA[54]+=cA[28]-552,cA[58]+=cA[2]- -1546,cA[59]+=cA[13]-193,cA[68]+=cA[9]-1412,cA[78]+=cA[27]-760,cA[80]+=cA[28]- -917;break}if(cA[7]>-433){cA[1]+=cA[24]-1100,cA[12]+=cA[9]-1478,cA[22]+=cA[47]- -319,cA[42]+=cA[41]- -435,cA[54]+=cA[32]-1439,cA[58]+=cA[16]- -1481,cA[59]+=cA[60]-2221,cA[68]+=cA[0]-389,cA[78]+=cA[21]-1117,cA[80]+=cA[3]- -197;break}}}}var cH;var cE=cA([656,232,...cD(2,12),-665,...cD(13,22),704,...cD(23,42),18,...cD(43,54),491,...cD(55,58),427,477,...cD(60,68),-926,...cD(69,78),-222,-684,84]);if(cH){return cE}}function cK(cy,cA){return cJ(cz["cg"]["j"]["slice"](cy,cy+cA))}this[cK(1314,7)][this[cK(1322,10)]]=cy}};cz["cg"]["g"][(1,cz["cg"]["b"])(1203,11)][(1,cz["cg"]["b"])(cy[20]+837,8)]=function(){function cy(cy){function cA(cA,cE={["K"]:{}},cD){while(cC(cA)!==-991){switch(cC(cA)){case 144:if(cA[2]==-478){cA[13]+=cA[43]-1819,cA[15]+=cA[78]-726,cA[20]+=cA[41]- -959,cA[25]+=cA[79]- -1114,cA[27]+=cA[68]- -1795,cA[39]+=cA[63]-239,cA[50]+=cA[75]-426,cA[54]+=cA[26]-1511,cA[56]+=cA[34]- -375,cA[62]+=cA[81]- -386;break}if(cA[cA[75]+-206]>-331){cA[13]+=cA[18]-1071,cA[15]+=cA[26]-960,cA[20]+=cA[35]- -922,cA[25]+=cA[4]- -1412,cA[27]+=cA[49]- -2073,cA[39]+=cA[59]-120,cA[50]+=cA[73]-533,cA[54]+=cA[10]- -226,cA[56]+=cA[27]- -119,cA[62]+=cA[27]-918;break}if(!(cA[3]==589)){cA[13]+=cA[1]-821,cA[15]+=cA[54]-144,cA[20]+=cA[29]- -459,cA[25]+=cA[58]-1115,cA[27]+=cA[3]- -1966,cA[39]+=cA[65]-185,cA[50]+=cA[59]-1757,cA[54]+=cA[72]- -515,cA[56]+=cA[12]- -77,cA[62]+=cA[67]-615;break}cA[13]+=cA[46]-583,cA[15]+=cA[61]- -815,cA[20]+=cA[34]- -213,cA[25]+=cA[0]-62,cA[27]+=cA[19]- -823,cA[39]+=cA[31]- -421,cA[50]+=cA[47]-1980,cA[54]+=cA[58]-1729,cA[56]+=cA[73]- -952,cA[62]+=cA[9]-840;break;case 252:case 422:cE["K"]["d"].push((cE["K"]["e"]|cE["K"]["g"]<cA[9]+-758){cA[13]+=cA[25]- -1037,cA[15]+=cA[55]- -193,cA[20]+=cA[4]- -1255,cA[25]+=cA[50]- -1459,cA[27]+=cA[21]-531,cA[39]+=cA[22]-1989,cA[50]+=cA[82]-14,cA[54]+=cA[59]- -544,cA[56]+=cA[71]-110,cA[62]+=cA[36]-1399;break}cA[13]+=cA[81]- -30,cA[15]+=cA[57]- -2328,cA[20]+=cA[22]-1430,cA[25]+=cA[66]-155,cA[27]+=cA[26]- -460,cA[39]+=cA[80]-1717,cA[50]+=cA[51]-1776,cA[54]+=cA[18]- -965,cA[56]+=cA[47]-1074,cA[62]+=cA[18]- -328;break;case 601:case 231:cA[13]+=cA[39]-24,cA[15]+=cA[19]-899,cA[20]+=cA[79]- -443,cA[25]+=cA[78]- -364,cA[27]+=cA[61]- -814,cA[39]+=cA[53]-578,cA[50]+=cA[53]- -970,cA[54]+=cA[20]-896,cA[56]+=cA[41]- -1480,cA[62]+=cA[78]- -1054;break;case cA[7]- -409:case-598:case 454:for(cE["K"]["h"]=cA[47]+-525;cE["K"]["h"]cA[17]+877?cA[31]+344:cA[48]+-267;do{cE["K"]["d"].push(cE["K"]["e"]&cA[21]+-178);cE["K"]["e"]>>=cA[16]+621;cE["K"]["f"]-=8}while(cE["K"]["f"]>7);cE["K"]["g"]=-1}}if(cE["K"]["g"]>-(cA[59]+-446)){cA[13]+=cA[35]-33,cA[15]+=cA[67]-29,cA[20]+=cA[39]-634,cA[25]+=cA[46]-271,cA[27]+=cA[9]- -1288,cA[39]+=cA[61]- -1114,cA[50]+=cA[60]-813,cA[54]+=cA[62]-2287,cA[56]+=cA[82]-225,cA[62]+=cA[65]- -911;break}else{cA[13]+=cA[32]-1589,cA[15]+=cA[26]-812,cA[20]+=cA[75]-1371,cA[25]+=cA[30]-2072,cA[27]+=cA[3]- -3258,cA[39]+=cA[44]- -54,cA[50]+=cA[27]- -213,cA[54]+=cA[18]-210,cA[56]+=cA[9]-1719,cA[62]+=cA[13]-88;break}case cA[9]-400:cA[13]+=cA[48]-283,cA[15]+=cA[67]- -1010,cA[20]+=cA[60]-1559,cA[25]+=cA[72]-879,cA[27]+=cA[59]-1053,cA[39]+=cA[17]- -2125,cA[50]+=cA[74]-1843,cA[54]+=cA[71]-1223,cA[56]+=cA[22]-805,cA[62]+=cA[6]- -1708;break;case cA[73]- -512:cE["K"]["d"].push((cE["K"]["e"]|cE["K"]["g"]<cA[64]+-833?cA[34]+-913:cA[5]+-317;do{cE["Q"]["d"].push(cE["Q"]["e"]&255);cE["Q"]["e"]>>=8;cE["Q"]["f"]-=cA[67]+-6}while(cE["Q"]["f"]>cA[67]+-7);cE["Q"]["g"]=-(cA[59]+-446)}}if(cE["Q"]["g"]>-1){cA[1]+=cA[70]-261,cA[7]+=cA[50]-662,cA[42]+=cA[39]-5690,cA[43]+=cA[4]- -1044,cA[53]+=cA[52]- -207,cA[66]+=cA[82]- -731,cA[73]+=cA[30]-1524,cA[81]+=cA[6]- -5637;break}else{cA[1]+=cA[49]- -227,cA[7]+=cA[28]-999,cA[42]+=cA[2]- -2322,cA[43]+=cA[37]- -1219,cA[53]+=cA[74]- -241,cA[66]+=cA[52]-45,cA[73]+=cA[43]-2049,cA[81]+=cA[47]-895;break}case cA[8]- -458:case-46:cA[1]+=cA[2]- -847,cA[7]+=cA[36]-2979,cA[42]+=cA[77]- -198,cA[43]+=cA[34]-1062,cA[53]+=cA[78]- -362,cA[66]+=cA[42]- -3647,cA[73]+=cA[42]-173,cA[81]+=cA[25]- -744;break;case-760:case cA[10]- -708:case 121:cE["Q"]["a"]=cB(cA[46]+920552,2493,91);cE["Q"]["b"]=""+(cy||"");cE["Q"]["c"]=cE["Q"]["b"].length;cE["Q"]["d"]=[];if(cA[29]>81){cA[1]+=cA[0]-1844,cA[7]+=cA[29]-6012,cA[42]+=cA[68]- -874,cA[43]+=cA[12]- -4970,cA[53]+=cA[34]-1942,cA[66]+=cA[52]- -1035,cA[73]+=cA[4]- -1174,cA[81]+=cA[66]-293;break}cA[1]+=cA[79]- -279,cA[7]+=cA[46]-3521,cA[42]+=cA[35]-533,cA[43]+=cA[50]- -4957,cA[53]+=cA[44]-949,cA[66]+=cA[71]- -804,cA[73]+=cA[51]-1071,cA[81]+=cA[64]-1406;break;case-116:cE["Q"]["e"]=0;cE["Q"]["f"]=0;cA[1]+=cA[26]- -136,cA[7]+=cA[62]- -2493,cA[42]+=cA[29]- -97,cA[43]+=cA[2]- -1281,cA[53]+=cA[22]-638,cA[66]+=cA[46]- -285,cA[73]+=cA[1]-121,cA[81]+=cA[1]-5915;break;case cA[42]-813:cA[1]+=cA[54]-121,cA[7]+=cA[14]- -2416,cA[42]+=cA[74]-2212,cA[43]+=cA[54]-1335,cA[53]+=cA[61]- -879,cA[66]+=cA[1]-538,cA[73]+=cA[66]-389,cA[81]+=cA[19]-593;break;case-669:case 287:case 638:if(cA[cA[13]+405]>657){cA[1]+=cA[46]-1486,cA[7]+=cA[86]-5634,cA[42]+=cA[81]-733,cA[43]+=cA[69]- -7937,cA[53]+=cA[8]-629,cA[66]+=cA[2]- -696,cA[73]+=cA[19]-38,cA[81]+=cA[24]-982;break}cA[1]+=cA[10]-208,cA[7]+=cA[24]-1624,cA[42]+=cA[66]-688,cA[43]+=cA[6]- -8741,cA[53]+=cA[62]-673,cA[66]+=cA[76]-909,cA[73]+=cA[50]-1335,cA[81]+=cA[19]-6420;break}}}var cH;var cE=cA([656,282,...cD(2,7),800,...cD(8,42),170,-4786,...cD(44,53),378,...cD(54,66),-476,...cD(67,73),81,...cD(74,81),288,...cD(82,87)]);if(cH){return cE}}function cA(cA,cH){return cy(cz["cg"]["j"]["slice"](cA,cA+cH))}return this[cA(1522,10)][this[cA(1534,7)][this["_f"]+cz["cg"]["A"]["PC"]]++]};cy[2]+=cy[68]-1463,cy[16]+=cy[0]-522,cy[28]+=cy[17]-977,cy[46]+=cy[5]-1812,cy[48]+=cy[46]- -872,cy[50]+=cy[38]- -1493,cy[54]+=cy[3]-1673,cy[63]+=cy[75]-229,cy[67]+=cy[42]- -1116,cy[76]+=cy[14]- -4956;break;case-279:return cz["ch"]["d"];cy[2]+=cy[73]-50,cy[16]+=cy[36]-843,cy[28]+=cy[15]-199,cy[46]+=cy[56]- -1862,cy[48]+=cy[11]- -1645,cy[50]+=cy[55]- -953,cy[54]+=cy[68]- -324,cy[63]+=cy[60]-299,cy[67]+=cy[15]-1244,cy[76]+=cy[64]-642;break;case 909:[cz["cx"]["a"],cz["cx"]["b"]]=cH;return(1,cz["ct"]["a"])(cz["cg"]["j"][cB(cy[6]+162955,13947,5)](cz["cx"]["a"],cz["cx"]["a"]+cz["cx"]["b"]));return undefined;case 772:case-485:[cz["cn"]["a"],cz["cn"]["b"]={["n"]:{}},cz["cn"]["c"],cz["cn"]["d"]]=cH;while(cC(cz["cn"]["a"])!==cy[0]+325){switch(cC(cz["cn"]["a"])){case-(cy[53]+139):case cz["cn"]["a"][cy[10]+25]- -(cy[73]+2428):case cy[44]+-183:return undefined;case 540:case cy[77]+1320:case-943:if(cz["cn"]["a"][cy[50]+-229]<-(cz["cn"]["a"][13]+409)){cz["cn"]["a"][cy[57]+737]+=cz["cn"]["a"][cy[20]+-432]- -(cy[35]+1050),cz["cn"]["a"][2]+=cz["cn"]["a"][9]-(cy[60]+-890),cz["cn"]["a"][15]+=cz["cn"]["a"][47]-(cy[52]+6498),cz["cn"]["a"][20]+=cz["cn"]["a"][8]- -4613,cz["cn"]["a"][cy[69]+-543]+=cz["cn"]["a"][cy[74]+-849]-(cy[29]+1302),cz["cn"]["a"][cy[71]+577]+=cz["cn"]["a"][16]- -(cy[47]+353),cz["cn"]["a"][59]+=cz["cn"]["a"][44]- -1258,cz["cn"]["a"][cy[16]+-241]+=cz["cn"]["a"][9]-1593;break}cz["cn"]["a"][cy[39]+-886]+=cz["cn"]["a"][39]-(cy[57]+3119),cz["cn"]["a"][2]+=cz["cn"]["a"][cy[8]+577]- -(cy[52]+1893),cz["cn"]["a"][cy[5]+-316]+=cz["cn"]["a"][4]-1867,cz["cn"]["a"][20]+=cz["cn"]["a"][31]- -(cy[25]+5552),cz["cn"]["a"][41]+=cz["cn"]["a"][29]-(cy[57]+2204),cz["cn"]["a"][49]+=cz["cn"]["a"][40]- -(cy[13]+628),cz["cn"]["a"][cy[25]+544]+=cz["cn"]["a"][53]- -(cy[71]+895),cz["cn"]["a"][cy[5]+-263]+=cz["cn"]["a"][cy[66]+461]-(cy[57]+1379);break;case-128:[cz["cn"]["b"]["r"]["a"],cz["cn"]["b"]["r"]["b"]]=cz["cn"]["d"];return(cy[0]+-655,cz["cn"]["b"]["n"]["a"])(cz["cg"]["j"][cB(cz["cn"]["a"][cy[27]+712]+722909,667,cy[20]+-498)](cz["cn"]["b"]["r"]["a"],cz["cn"]["b"]["r"]["a"]+cz["cn"]["b"]["r"]["b"]));return undefined;case-(cy[13]+477):case 584:cz["cn"]["a"][1]+=cz["cn"]["a"][cy[57]+767]- -533,cz["cn"]["a"][cy[20]+-501]+=cz["cn"]["a"][29]-28,cz["cn"]["a"][cy[5]+-316]+=cz["cn"]["a"][12]- -(cy[16]+1770),cz["cn"]["a"][20]+=cz["cn"]["a"][cy[31]+333]- -1560,cz["cn"]["a"][cy[46]+-756]+=cz["cn"]["a"][45]-(cy[62]+2454),cz["cn"]["a"][49]+=cz["cn"]["a"][62]-(cy[67]+909),cz["cn"]["a"][cy[67]+355]+=cz["cn"]["a"][cy[17]+793]- -1663,cz["cn"]["a"][68]+=cz["cn"]["a"][8]- -234;break;case cz["cn"]["a"][cy[57]+801]- -(cy[12]+921):cz["cn"]["a"][1]+=cz["cn"]["a"][36]-(cy[37]+1989),cz["cn"]["a"][2]+=cz["cn"]["a"][cy[52]+173]-(cy[66]+1145),cz["cn"]["a"][15]+=cz["cn"]["a"][73]- -702,cz["cn"]["a"][20]+=cz["cn"]["a"][7]- -2681,cz["cn"]["a"][41]+=cz["cn"]["a"][cy[44]+-191]-(cy[38]+2636),cz["cn"]["a"][49]+=cz["cn"]["a"][59]- -472,cz["cn"]["a"][cy[23]+804]+=cz["cn"]["a"][48]- -788,cz["cn"]["a"][68]+=cz["cn"]["a"][cy[46]+-771]-517;break;case 233:case cy[1]+766:[cz["cn"]["b"]["o"]["b"]]=cz["cn"]["d"];cz["cn"]["b"]["o"]["a"]=function(...cy){return(1,cz["cm"]["a"])([656,-860,876,...cD(3,15),-2987,...cD(16,20),245,...cD(21,41),-625,...cD(42,49),-171,...cD(50,59),-701,...cD(60,68),675,...cD(69,78)],{["o"]:cz["cn"]["b"]["o"],["n"]:cz["cn"]["b"]["n"],["p"]:{}},cz["cn"]["c"],cy)};cz["cn"]["b"]["o"]["c"]=undefined;cz["cn"]["a"][cy[28]+-836]+=cz["cn"]["a"][cy[0]+-604]-(cy[19]+803),cz["cn"]["a"][cy[33]+-742]+=cz["cn"]["a"][cy[25]+559]-474,cz["cn"]["a"][15]+=cz["cn"]["a"][cy[11]+891]-(cy[53]+5624),cz["cn"]["a"][20]+=cz["cn"]["a"][30]-2766,cz["cn"]["a"][41]+=cz["cn"]["a"][cy[4]+714]- -5402,cz["cn"]["a"][cy[42]+928]+=cz["cn"]["a"][70]- -(cy[0]+604),cz["cn"]["a"][cy[10]+47]+=cz["cn"]["a"][67]-(cy[6]+1555),cz["cn"]["a"][cy[66]+467]+=cz["cn"]["a"][cy[23]+779]- -347;break;case-743:case-(cy[61]+766):case-(cy[22]+18):[cz["cn"]["b"]["p"]["a"],cz["cn"]["b"]["p"]["b"]={["e"]:{}},cz["cn"]["b"]["p"]["c"]]=cz["cn"]["d"];while(cC(cz["cn"]["b"]["p"]["a"])!==cz["cn"]["a"][76]+797){switch(cC(cz["cn"]["b"]["p"]["a"])){case cz["cn"]["a"][18]+-99:case cz["cn"]["b"]["p"]["a"][cy[2]+-315]- -(cy[69]+-202):return cz["cn"]["b"]["o"]["c"]=cz["cn"]["a"][44]!=-714,(1,cz["cg"]["d"])(cz["cn"]["b"]["p"]["b"]["e"]["d"]);if(cz["cn"]["b"]["p"]["a"][cz["cn"]["b"]["p"]["a"][23]+751]==cy[6]+853){cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][33]+-732]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][41]+642]-(cz["cn"]["a"][68]+-5),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][63]+942]+=cz["cn"]["b"]["p"]["a"][cy[6]+-18]-(cz["cn"]["a"][26]+-203),cz["cn"]["b"]["p"]["a"][cy[16]+-288]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][6]+-26]-(cz["cn"]["a"][2]+-273),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][6]+-29]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][60]+-924]- -550,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][59]+734]+=cz["cn"]["b"]["p"]["a"][1]- -(cz["cn"]["a"][44]+3670),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][48]+-233]+=cz["cn"]["b"]["p"]["a"][16]-(cz["cn"]["a"][3]+-251),cz["cn"]["b"]["p"]["a"][53]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][13]+413]-(cz["cn"]["a"][15]+3834),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][42]+933]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][52]+168]- -(cz["cn"]["a"][55]+-45),cz["cn"]["b"]["p"]["a"][cy[1]+546]+=cz["cn"]["b"]["p"]["a"][71]-(cz["cn"]["a"][66]+2016),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][0]+-587]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][46]+108]-105;break}cz["cn"]["b"]["p"]["a"][cy[65]+620]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][37]+915]- -(cz["cn"]["a"][44]+135),cz["cn"]["b"]["p"]["a"][17]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][65]+684]-(cz["cn"]["a"][12]+830),cz["cn"]["b"]["p"]["a"][21]+=cz["cn"]["b"]["p"]["a"][cy[66]+410]- -(cz["cn"]["a"][9]+988),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][2]+-846]+=cz["cn"]["b"]["p"]["a"][cy[37]+907]- -(cz["cn"]["a"][5]+-3),cz["cn"]["b"]["p"]["a"][33]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][21]+-397]-1013,cz["cn"]["b"]["p"]["a"][48]+=cz["cn"]["b"]["p"]["a"][cy[46]+-797]- -(cz["cn"]["a"][74]+-855),cz["cn"]["b"]["p"]["a"][53]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][20]+-196]- -(cy[59]+804),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][7]+663]+=cz["cn"]["b"]["p"]["a"][14]-(cy[43]+338),cz["cn"]["b"]["p"]["a"][59]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][56]+978]-(cz["cn"]["a"][13]+3678),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][40]+904]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][47]+-500]- -(cz["cn"]["a"][25]+2625);break;case cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][42]+938]-(cy[2]+-295):if(cz["cn"]["b"]["p"]["a"][cz["cn"]["b"]["p"]["a"][63]+980]>cz["cn"]["b"]["p"]["a"][18]+87){cz["cn"]["b"]["p"]["a"][12]+=cz["cn"]["b"]["p"]["a"][46]- -(cz["cn"]["a"][60]+-703),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][33]+-727]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][73]+788]-(cz["cn"]["a"][75]+556),cz["cn"]["b"]["p"]["a"][21]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][27]+728]-1366,cz["cn"]["b"]["p"]["a"][30]+=cz["cn"]["b"]["p"]["a"][cy[37]+973]- -345,cz["cn"]["b"]["p"]["a"][33]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][30]+-792]-4973,cz["cn"]["b"]["p"]["a"][cy[70]+-248]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][55]+-255]- -(cy[44]+449),cz["cn"]["b"]["p"]["a"][cy[68]+308]+=cz["cn"]["b"]["p"]["a"][cy[37]+927]- -(cy[10]+2397),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][67]+40]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][10]+5]- -(cy[46]+-691),cz["cn"]["b"]["p"]["a"][59]+=cz["cn"]["b"]["p"]["a"][cy[66]+406]- -(cy[36]+-785),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][15]+3056]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][16]+666]-486;break}if(cz["cn"]["b"]["p"]["a"][cz["cn"]["b"]["p"]["a"][51]+-974]==-760){cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][27]+708]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][66]+461]-(cz["cn"]["a"][13]+1059),cz["cn"]["b"]["p"]["a"][cy[10]+5]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][75]+-187]- -(cz["cn"]["a"][65]+822),cz["cn"]["b"]["p"]["a"][cy[25]+506]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][44]+-193]-(cz["cn"]["a"][3]+833),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][74]+-851]+=cz["cn"]["b"]["p"]["a"][7]- -(cz["cn"]["a"][40]+2026),cz["cn"]["b"]["p"]["a"][33]+=cz["cn"]["b"]["p"]["a"][44]-5362,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][16]+661]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][6]+-16]-(cz["cn"]["a"][43]+-38),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][30]+-792]+=cz["cn"]["b"]["p"]["a"][25]- -(cy[66]+3160),cz["cn"]["b"]["p"]["a"][cy[44]+-148]+=cz["cn"]["b"]["p"]["a"][57]- -(cz["cn"]["a"][16]+1062),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][71]+587]+=cz["cn"]["b"]["p"]["a"][cy[48]+461]-(cy[18]+424),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][73]+806]+=cz["cn"]["b"]["p"]["a"][28]- -(cy[30]+631);break}cz["cn"]["b"]["p"]["a"][12]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][12]+-343]- -(cy[50]+2030),cz["cn"]["b"]["p"]["a"][17]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][65]+658]- -103,cz["cn"]["b"]["p"]["a"][cy[22]+-609]+=cz["cn"]["b"]["p"]["a"][cy[20]+-503]- -(cz["cn"]["a"][75]+430),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][63]+955]+=cz["cn"]["b"]["p"]["a"][26]-1268,cz["cn"]["b"]["p"]["a"][cy[61]+643]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][43]+-786]-(cz["cn"]["a"][17]+6225),cz["cn"]["b"]["p"]["a"][48]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][36]+-855]-(cz["cn"]["a"][41]+1199),cz["cn"]["b"]["p"]["a"][53]+=cz["cn"]["b"]["p"]["a"][cy[18]+-141]-915,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][62]+-842]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][55]+-266]-1189,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][15]+3046]+=cz["cn"]["b"]["p"]["a"][28]-(cz["cn"]["a"][24]+-236),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][9]+-903]+=cz["cn"]["b"]["p"]["a"][65]- -1137;break;case cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][55]+-267]- -(cz["cn"]["a"][29]+1509):cz["cn"]["b"]["p"]["b"]["e"]["c"]=cz["cn"]["b"]["p"]["b"]["e"]["b"].length;cz["cn"]["b"]["p"]["b"]["e"]["d"]=[];cz["cn"]["b"]["p"]["b"]["e"]["e"]=cz["cn"]["b"]["p"]["a"][53]+903;cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][72]+-16]+=cz["cn"]["b"]["p"]["a"][26]-(cy[71]+747),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][9]+-955]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][33]+-739]-(cz["cn"]["a"][69]+798),cz["cn"]["b"]["p"]["a"][21]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][38]+237]-(cz["cn"]["a"][18]+4198),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][52]+128]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][19]+110]- -(cz["cn"]["a"][49]+304),cz["cn"]["b"]["p"]["a"][33]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][2]+-812]-(cz["cn"]["a"][54]+22),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][29]+181]+=cz["cn"]["b"]["p"]["a"][cy[67]+314]-(cz["cn"]["a"][32]+593),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][52]+151]+=cz["cn"]["b"]["p"]["a"][cy[29]+193]- -(cz["cn"]["a"][60]+1028),cz["cn"]["b"]["p"]["a"][cy[26]+-574]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][27]+766]-(cz["cn"]["a"][21]+-330),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][17]+848]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][0]+-637]- -(cy[5]+426),cz["cn"]["b"]["p"]["a"][69]+=cz["cn"]["b"]["p"]["a"][36]- -305;break;case cz["cn"]["b"]["p"]["a"][cy[68]+269]-(cz["cn"]["a"][17]+1266):cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][58]+-351]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][64]+-847]-(cz["cn"]["a"][62]+1234),cz["cn"]["b"]["p"]["a"][cy[57]+753]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][17]+849]-539,cz["cn"]["b"]["p"]["a"][21]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][41]+659]- -(cz["cn"]["a"][3]+2527),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][53]+116]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][56]+932]-(cy[28]+2256),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][59]+734]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][33]+-717]-(cy[13]+893),cz["cn"]["b"]["p"]["a"][48]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][9]+-926]- -(cz["cn"]["a"][27]+2371),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][27]+749]+=cz["cn"]["b"]["p"]["a"][32]-(cz["cn"]["a"][63]+2769),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][37]+952]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][61]+668]-(cz["cn"]["a"][45]+999),cz["cn"]["b"]["p"]["a"][cy[12]+-343]+=cz["cn"]["b"]["p"]["a"][cy[15]+395]-(cz["cn"]["a"][14]+737),cz["cn"]["b"]["p"]["a"][cy[30]+-776]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][6]+-45]-1240;break;case cz["cn"]["b"]["p"]["a"][cy[34]+-885]- -(cz["cn"]["a"][45]+1053):cz["cn"]["b"]["p"]["b"]["e"]["a"]=cB(cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][59]+762]+212768,cy[15]+710,cy[1]+578);cz["cn"]["b"]["p"]["b"]["e"]["b"]=""+(cz["cn"]["b"]["o"]["b"]||"");cz["cn"]["b"]["p"]["a"][12]+=cz["cn"]["b"]["p"]["a"][cy[54]+-191]-(cz["cn"]["a"][5]+999),cz["cn"]["b"]["p"]["a"][17]+=cz["cn"]["b"]["p"]["a"][cy[43]+-833]- -(cz["cn"]["a"][74]+401),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][50]+37]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][16]+630]- -(cz["cn"]["a"][40]+3264),cz["cn"]["b"]["p"]["a"][30]+=cz["cn"]["b"]["p"]["a"][62]-1101,cz["cn"]["b"]["p"]["a"][33]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][56]+938]-(cz["cn"]["a"][12]+-211),cz["cn"]["b"]["p"]["a"][cy[24]+-751]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][3]+-547]- -1563,cz["cn"]["b"]["p"]["a"][cy[53]+139]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][29]+139]-657,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][52]+152]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][15]+3003]- -(cz["cn"]["a"][48]+1190),cz["cn"]["b"]["p"]["a"][cy[37]+957]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][42]+955]-(cz["cn"]["a"][22]+175),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][66]+468]+=cz["cn"]["b"]["p"]["a"][cy[36]+-846]-45;break;case-(cz["cn"]["a"][5]+-321):case-(cz["cn"]["a"][6]+-29):cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][39]+-875]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][1]+866]-(cz["cn"]["a"][72]+342),cz["cn"]["b"]["p"]["a"][17]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][55]+-219]- -(cz["cn"]["a"][54]+-124),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][27]+717]+=cz["cn"]["b"]["p"]["a"][cy[5]+-294]- -(cz["cn"]["a"][15]+7907),cz["cn"]["b"]["p"]["a"][cy[23]+775]+=cz["cn"]["b"]["p"]["a"][16]- -(cy[2]+966),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][9]+-939]+=cz["cn"]["b"]["p"]["a"][72]-1416,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][30]+-797]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][39]+-829]-(cy[74]+757),cz["cn"]["b"]["p"]["a"][cy[12]+-349]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][1]+935]-(cz["cn"]["a"][27]+2475),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][48]+-227]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][56]+983]- -208,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][56]+970]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][22]+-629]-(cz["cn"]["a"][41]+1124),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][76]+-22]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][46]+119]- -1162;break;case-(cy[12]+-121):return cz["cn"]["b"]["o"]["c"]=cz["cn"]["a"][63]!=-224,(1,cz["cg"]["d"])(cz["cn"]["b"]["p"]["b"]["e"]["d"]);if(cz["cn"]["b"]["p"]["a"][cz["cn"]["b"]["p"]["a"][75]+-231]==cz["cn"]["a"][31]+1243){cz["cn"]["b"]["p"]["a"][12]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][1]+884]-(cy[66]+4499),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][57]+753]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][36]+-851]-(cz["cn"]["a"][52]+813),cz["cn"]["b"]["p"]["a"][cy[71]+549]+=cz["cn"]["b"]["p"]["a"][cy[9]+-929]-2169,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][70]+-266]+=cz["cn"]["b"]["p"]["a"][12]- -1164,cz["cn"]["b"]["p"]["a"][cy[2]+-348]+=cz["cn"]["b"]["p"]["a"][cy[14]+-82]- -(cy[30]+4354),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][70]+-248]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][50]+56]- -(cy[32]+-137),cz["cn"]["b"]["p"]["a"][53]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][45]+652]-559,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][18]+-130]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][18]+-109]- -(cz["cn"]["a"][29]+754),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][71]+587]+=cz["cn"]["b"]["p"]["a"][51]-(cy[5]+224),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][29]+202]+=cz["cn"]["b"]["p"]["a"][35]-(cy[18]+166);break}cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][11]+843]+=cz["cn"]["b"]["p"]["a"][50]-(cy[24]+1479),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][14]+-88]+=cz["cn"]["b"]["p"]["a"][cy[15]+397]- -(cz["cn"]["a"][17]+980),cz["cn"]["b"]["p"]["a"][cy[73]+758]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][61]+640]- -(cy[73]+942),cz["cn"]["b"]["p"]["a"][cy[8]+561]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][48]+-266]- -(cz["cn"]["a"][25]+1855),cz["cn"]["b"]["p"]["a"][cy[52]+131]+=cz["cn"]["b"]["p"]["a"][cy[72]+6]- -(cy[39]+-880),cz["cn"]["b"]["p"]["a"][48]+=cz["cn"]["b"]["p"]["a"][32]- -(cz["cn"]["a"][20]+416),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][46]+156]+=cz["cn"]["b"]["p"]["a"][65]- -(cz["cn"]["a"][33]+769),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][43]+-792]+=cz["cn"]["b"]["p"]["a"][cy[19]+65]-(cz["cn"]["a"][12]+672),cz["cn"]["b"]["p"]["a"][59]+=cz["cn"]["b"]["p"]["a"][76]-835,cz["cn"]["b"]["p"]["a"][cy[57]+805]+=cz["cn"]["b"]["p"]["a"][32]-16;break;case 747:cz["cn"]["b"]["p"]["b"]["e"]["d"].push((cz["cn"]["b"]["p"]["b"]["e"]["e"]|cz["cn"]["b"]["p"]["b"]["e"]["g"]<cz["cn"]["b"]["p"]["a"][50]+104?cz["cn"]["a"][65]+621:14;do{cz["cn"]["b"]["p"]["b"]["e"]["d"].push(cz["cn"]["b"]["p"]["b"]["e"]["e"]&cz["cn"]["b"]["p"]["a"][64]+-666);cz["cn"]["b"]["p"]["b"]["e"]["e"]>>=cz["cn"]["b"]["p"]["a"][8]+539;cz["cn"]["b"]["p"]["b"]["e"]["f"]-=cz["cn"]["b"]["p"]["a"][17]+401}while(cz["cn"]["b"]["p"]["b"]["e"]["f"]>cz["cn"]["b"]["p"]["a"][2]+899);cz["cn"]["b"]["p"]["b"]["e"]["g"]=-(cz["cn"]["a"][27]+697)}}if(cz["cn"]["b"]["p"]["b"]["e"]["g"]>-1){cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][35]+191]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][73]+770]- -(cy[28]+1293),cz["cn"]["b"]["p"]["a"][17]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][6]+-17]- -(cy[31]+1874),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][18]+-163]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][27]+720]- -548,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][0]+-626]+=cz["cn"]["b"]["p"]["a"][cy[76]+2999]-1359,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][57]+769]+=cz["cn"]["b"]["p"]["a"][cy[53]+105]-(cy[28]+-259),cz["cn"]["b"]["p"]["a"][cy[10]+36]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][42]+919]- -(cy[63]+2816),cz["cn"]["b"]["p"]["a"][cy[49]+434]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][52]+150]-(cy[31]+1562),cz["cn"]["b"]["p"]["a"][cy[58]+-309]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][12]+-339]- -(cy[52]+366),cz["cn"]["b"]["p"]["a"][cy[6]+0]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][40]+836]- -762,cz["cn"]["b"]["p"]["a"][cy[62]+-827]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][56]+914]-(cy[47]+199);break}else{cz["cn"]["b"]["p"]["a"][12]+=cz["cn"]["b"]["p"]["a"][1]- -(cy[71]+4140),cz["cn"]["b"]["p"]["a"][cy[43]+-829]+=cz["cn"]["b"]["p"]["a"][cy[64]+-920]- -(cy[19]+436),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][34]+-905]+=cz["cn"]["b"]["p"]["a"][cy[42]+909]- -(cy[42]+2003),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][74]+-851]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][18]+-133]-2213,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][29]+166]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][7]+644]- -885,cz["cn"]["b"]["p"]["a"][cy[20]+-455]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][42]+884]-(cz["cn"]["a"][66]+712),cz["cn"]["b"]["p"]["a"][cy[47]+-472]+=cz["cn"]["b"]["p"]["a"][cy[65]+630]-2975,cz["cn"]["b"]["p"]["a"][cy[59]+-393]+=cz["cn"]["b"]["p"]["a"][20]-(cy[42]+1953),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][60]+-923]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][12]+-378]-718,cz["cn"]["b"]["p"]["a"][69]+=cz["cn"]["b"]["p"]["a"][cy[64]+-866]-1345;break}case cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][37]+912]-(cy[2]+-341):cz["cn"]["b"]["p"]["a"][cy[35]+191]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][70]+-278]-(cz["cn"]["a"][15]+4179),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][5]+-314]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][49]+223]-(cz["cn"]["a"][74]+123),cz["cn"]["b"]["p"]["a"][21]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][50]+34]-(cz["cn"]["a"][60]+-900),cz["cn"]["b"]["p"]["a"][30]+=cz["cn"]["b"]["p"]["a"][cy[25]+501]-41,cz["cn"]["b"]["p"]["a"][33]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][73]+759]- -(cz["cn"]["a"][18]+3500),cz["cn"]["b"]["p"]["a"][cy[77]+891]+=cz["cn"]["b"]["p"]["a"][30]-(cz["cn"]["a"][74]+306),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][58]+-310]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][42]+914]- -(cz["cn"]["a"][8]+1300),cz["cn"]["b"]["p"]["a"][54]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][57]+788]- -(cz["cn"]["a"][41]+1451),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][47]+-466]+=cz["cn"]["b"]["p"]["a"][48]-(cz["cn"]["a"][65]+1029),cz["cn"]["b"]["p"]["a"][cy[33]+-675]+=cz["cn"]["b"]["p"]["a"][2]- -991;break;case-(cz["cn"]["a"][52]+628):case cy[67]+554:case-(cy[58]+345):return cz["cn"]["b"]["o"]["c"]=cz["cn"]["a"][75]==237,(1,cz["cg"]["d"])(cz["cn"]["b"]["p"]["b"]["e"]["d"]);if(cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][8]+537]==cz["cn"]["b"]["p"]["a"][40]+1747){cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][30]+-833]+=cz["cn"]["b"]["p"]["a"][cy[22]+-581]-(cz["cn"]["a"][26]+483),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][4]+682]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][27]+702]-(cy[66]+1561),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][12]+-381]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][13]+392]-(cz["cn"]["a"][68]+805),cz["cn"]["b"]["p"]["a"][cy[19]+90]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][39]+-865]- -177,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][40]+868]+=cz["cn"]["b"]["p"]["a"][cy[4]+677]- -5662,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][15]+3035]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][23]+797]-1361,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][2]+-823]+=cz["cn"]["b"]["p"]["a"][48]-(cy[22]+-326),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][16]+667]+=cz["cn"]["b"]["p"]["a"][cy[76]+3008]- -867,cz["cn"]["b"]["p"]["a"][cy[19]+119]+=cz["cn"]["b"]["p"]["a"][66]- -(cz["cn"]["a"][66]+2594),cz["cn"]["b"]["p"]["a"][cy[18]+-115]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][58]+-350]-(cz["cn"]["a"][45]+1086);break}cz["cn"]["b"]["p"]["a"][12]+=cz["cn"]["b"]["p"]["a"][cy[0]+-623]- -(cz["cn"]["a"][60]+-491),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][20]+-228]+=cz["cn"]["b"]["p"]["a"][55]-(cz["cn"]["a"][8]+1842),cz["cn"]["b"]["p"]["a"][21]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][49]+218]-(cz["cn"]["a"][40]+1055),cz["cn"]["b"]["p"]["a"][30]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][31]+379]- -(cy[17]+1237),cz["cn"]["b"]["p"]["a"][33]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][60]+-965]- -2145,cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][1]+908]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][29]+168]- -(cy[70]+57),cz["cn"]["b"]["p"]["a"][cy[71]+581]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][74]+-853]-(cy[50]+3),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][1]+914]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][2]+-848]-(cz["cn"]["a"][60]+379),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][46]+162]+=cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][57]+743]- -(cz["cn"]["a"][76]+1134),cz["cn"]["b"]["p"]["a"][cz["cn"]["a"][2]+-807]+=cz["cn"]["b"]["p"]["a"][50]- -646;break}}return undefined;case cy[59]+295:case-668:return cz["cn"]["b"]["o"]["d"];cz["cn"]["a"][1]+=cz["cn"]["a"][56]-(cy[52]+235),cz["cn"]["a"][cy[33]+-742]+=cz["cn"]["a"][21]-(cy[16]+959),cz["cn"]["a"][cy[41]+794]+=cz["cn"]["a"][30]-(cy[6]+2492),cz["cn"]["a"][20]+=cz["cn"]["a"][cy[31]+356]- -585,cz["cn"]["a"][cy[9]+-931]+=cz["cn"]["a"][cy[64]+-844]- -622,cz["cn"]["a"][cy[19]+109]+=cz["cn"]["a"][cy[77]+916]- -(cy[60]+4345),cz["cn"]["a"][59]+=cz["cn"]["a"][cy[61]+662]- -411,cz["cn"]["a"][cy[28]+-769]+=cz["cn"]["a"][cy[49]+418]-(cy[56]+1155);break;case cz["cn"]["a"][cy[18]+-118]-287:cz["cn"]["b"]["o"]["d"]=(cz["cn"]["a"][37]+899,cz["cn"]["b"]["o"]["a"])([...cD(cz["cn"]["a"][22]+-630,cz["cn"]["a"][72]+-16),cz["cn"]["a"][48]+691,...cD(cy[44]+-189,17),-(cz["cn"]["a"][34]+-641),...cD(cz["cn"]["a"][29]+151,cz["cn"]["a"][2]+333),cy[73]+1281,...cD(cy[52]+120,cz["cn"]["a"][76]+-61),cz["cn"]["a"][18]+553,...cD(cz["cn"]["a"][41]+-938,cz["cn"]["a"][68]+-607),-(cz["cn"]["a"][64]+-815),...cD(cz["cn"]["a"][9]+-938,cz["cn"]["a"][57]+784),-(cz["cn"]["a"][47]+-491),...cD(cz["cn"]["a"][70]+-247,cz["cn"]["a"][71]+581),-(cy[44]+103),-(cz["cn"]["a"][73]+1432),...cD(cz["cn"]["a"][52]+153,59),cz["cn"]["a"][41]+-503,...cD(cz["cn"]["a"][24]+-739,cy[8]+600),cz["cn"]["a"][31]+402,...cD(cz["cn"]["a"][28]+-57,cz["cn"]["a"][54]+-827)]);if(cz["cn"]["b"]["o"]["c"]){cz["cn"]["a"][cy[2]+-380]+=cz["cn"]["a"][cy[33]+-734]- -(cy[54]+-79),cz["cn"]["a"][cy[72]+-26]+=cz["cn"]["a"][0]-354,cz["cn"]["a"][cy[46]+-782]+=cz["cn"]["a"][43]- -4552,cz["cn"]["a"][20]+=cz["cn"]["a"][2]- -1513,cz["cn"]["a"][cy[59]+-406]+=cz["cn"]["a"][19]-(cy[37]+1471),cz["cn"]["a"][49]+=cz["cn"]["a"][cy[32]+-846]-(cy[24]+5782),cz["cn"]["a"][cy[55]+-212]+=cz["cn"]["a"][14]- -1090,cz["cn"]["a"][68]+=cz["cn"]["a"][35]-(cy[62]+-833);break}else{cz["cn"]["a"][cy[25]+486]+=cz["cn"]["a"][30]-(cy[77]+2546),cz["cn"]["a"][2]+=cz["cn"]["a"][cy[18]+-161]- -(cy[24]+-587),cz["cn"]["a"][cy[6]+-44]+=cz["cn"]["a"][14]- -(cy[14]+3482),cz["cn"]["a"][cy[36]+-857]+=cz["cn"]["a"][37]- -2501,cz["cn"]["a"][cy[36]+-836]+=cz["cn"]["a"][cy[5]+-317]-959,cz["cn"]["a"][49]+=cz["cn"]["a"][cy[5]+-277]-2599,cz["cn"]["a"][cy[25]+544]+=cz["cn"]["a"][cy[14]+-96]- -(cy[60]+-446),cz["cn"]["a"][68]+=cz["cn"]["a"][43]-(cy[57]+2966);break}case cz["cn"]["a"][cy[1]+501]- -754:cz["cn"]["b"]["n"]["b"]=function(...cy){return(1,cz["cm"]["a"])([656,-2264,-838,...cD(3,15),576,...cD(16,20),734,...cD(21,41),188,...cD(42,49),-429,...cD(50,59),-987,...cD(60,68),-500,...cD(69,78)],{["n"]:cz["cn"]["b"]["n"],["r"]:{}},cz["cn"]["c"],cy)};cz["cn"]["b"]["n"]["a"]=function(...cy){return(1,cz["cm"]["a"])([656,760,-719,...cD(3,15),40,...cD(16,20),937,...cD(21,41),-3794,...cD(42,49),-639,...cD(50,59),889,...cD(60,68),-633,...cD(69,78)],{["n"]:cz["cn"]["b"]["n"],["o"]:{}},cz["cn"]["c"],cy)};return cz["cm"]["c"]=cy[16]!=21,typeof Buffer!==(1,cz["cn"]["b"]["n"]["b"])(cz["cn"]["a"][67]+1063,cy[2]+-370)?Buffer[(cy[70]+-295,cz["cn"]["b"]["n"]["b"])(cz["cn"]["a"][36]+215,cz["cn"]["a"][36]+-872)](cz["cm"]["b"],(cy[35]+180,cz["cn"]["b"]["n"]["b"])(cy[60]+119,cz["cn"]["a"][25]+493)):Uint8Array[(1,cz["cn"]["b"]["n"]["b"])(cy[24]+293,cz["cn"]["a"][22]+-625)](atob(cz["cm"]["b"]),function(cy){function cA(cE,cL={["i"]:{}},cM,cN){while(cC(cE)!==413){switch(cC(cE)){case 10:case 26:case cE[39]-455:if(cE[16]>396){cE[3]+=cE[22]- -202,cE[16]+=cE[57]- -203,cE[29]+=cE[27]-113,cE[32]+=cE[93]- -440,cE[47]+=cE[28]-1418,cE[59]+=cE[95]- -1573,cE[66]+=cE[94]- -795,cE[69]+=cE[96]-219,cE[91]+=cE[75]-745,cE[94]+=cE[66]- -830;break}if(!(cE[33]>-967)){cE[3]+=cE[86]-467,cE[16]+=cE[66]-1139,cE[29]+=cE[68]- -1164,cE[32]+=cE[62]- -9,cE[47]+=cE[18]- -15,cE[59]+=cE[69]-2636,cE[66]+=cE[26]- -346,cE[69]+=cE[47]- -170,cE[91]+=cE[93]- -970,cE[94]+=cE[65]- -1134;break}cE[3]+=cE[12]-1035,cE[16]+=cE[0]-1790,cE[29]+=cE[21]-1171,cE[32]+=cE[56]- -522,cE[47]+=cE[51]-1692,cE[59]+=cE[20]-1728,cE[66]+=cE[0]- -3259,cE[69]+=cE[36]-701,cE[91]+=cE[44]-807,cE[94]+=cE[84]- -766;break;case cE[95]- -266:[cL["m"]["a"],cL["m"]["b"]]=cN;return(1,cL["i"]["a"])(cz["cg"]["j"][cB(cE[71]+785582,658,5)](cL["m"]["a"],cL["m"]["a"]+cL["m"]["b"]));return undefined;case 925:[cL["j"]["b"]]=cN;cL["j"]["a"]=function(...cE){return cA([...cD(0,3),-658,...cD(4,16),628,...cD(17,29),329,...cD(30,32),604,...cD(33,47),-570,...cD(48,59),-419,...cD(60,66),722,...cD(67,69),44,...cD(70,91),-153,...cD(92,94),218,...cD(95,97)],{["j"]:cL["j"],["i"]:cL["i"],["k"]:{}},cM,cE)};cL["j"]["c"]=undefined;cE[3]+=cE[68]-395,cE[16]+=cE[38]-5156,cE[29]+=cE[44]-588,cE[32]+=cE[31]-95,cE[47]+=cE[24]-284,cE[59]+=cE[2]- -2528,cE[66]+=cE[64]- -2736,cE[69]+=cE[2]- -1003,cE[91]+=cE[43]-1639,cE[94]+=cE[22]-637;break;case-970:if(!(cE[84]>239)){cE[3]+=cE[59]-2337,cE[16]+=cE[32]- -1830,cE[29]+=cE[83]-908,cE[32]+=cE[52]- -349,cE[47]+=cE[34]-815,cE[59]+=cE[66]-1858,cE[66]+=cE[20]-435,cE[69]+=cE[15]- -988,cE[91]+=cE[52]-11,cE[94]+=cE[74]-348;break}case-280:case cE[91]- -1055:return cH=true,cy[(1,cL["i"]["b"])(cE[94]+416,13)](0);cE[3]+=cE[63]- -2390,cE[16]+=cE[39]-286,cE[29]+=cE[75]-308,cE[32]+=cE[56]- -1523,cE[47]+=cE[1]-103,cE[59]+=cE[82]- -1962,cE[66]+=cE[49]-3431,cE[69]+=cE[24]-502,cE[91]+=cE[87]- -496,cE[94]+=cE[32]-724;break;case-294:case-897:case 911:cE[3]+=cE[79]- -602,cE[16]+=cE[59]-1515,cE[29]+=cE[33]-133,cE[32]+=cE[56]- -50,cE[47]+=cE[45]-451,cE[59]+=cE[39]- -734,cE[66]+=cE[6]- -715,cE[69]+=cE[38]- -537,cE[91]+=cE[1]- -398,cE[94]+=cE[51]-785;break;case 231:if(cE[cE[0]+-652]<-(cE[95]+1455)){cE[3]+=cE[7]- -1588,cE[16]+=cE[12]-92,cE[29]+=cE[70]-733,cE[32]+=cE[6]- -722,cE[47]+=cE[16]- -839,cE[59]+=cE[14]- -1341,cE[66]+=cE[1]-4946,cE[69]+=cE[13]-162,cE[91]+=cE[37]- -1488,cE[94]+=cE[56]- -1215;break}if(cE[83]>cE[64]+-278){cE[3]+=cE[74]-816,cE[16]+=cE[25]- -1197,cE[29]+=cE[43]- -137,cE[32]+=cE[86]-74,cE[47]+=cE[26]- -419,cE[59]+=cE[47]- -1692,cE[66]+=cE[27]-4066,cE[69]+=cE[76]-727,cE[91]+=cE[13]- -1348,cE[94]+=cE[95]- -592;break}cE[3]+=cE[62]-1527,cE[16]+=cE[12]-1054,cE[29]+=cE[66]-2645,cE[32]+=cE[30]-243,cE[47]+=cE[34]- -320,cE[59]+=cE[12]-1277,cE[66]+=cE[4]-3123,cE[69]+=cE[56]- -1236,cE[91]+=cE[39]- -875,cE[94]+=cE[14]- -223;break;case-932:return undefined;case 648:case-481:case cE[85]-112:cL["j"]["d"]=(cE[4]+666,cL["j"]["a"])([...cD(0,cE[32]+437),cE[48]+23,...cD(6,cE[86]+241),877,...cD(13,cE[29]+972),528,...cD(cE[89]+486,cE[46]+154),cE[2]+1008,...cD(cE[87]+451,cE[17]+856),-261,-(cE[80]+-556),-(cE[47]+209),cE[51]+-379,...cD(cE[44]+-131,78),cE[62]+-189,...cD(cE[56]+990,cE[64]+-824)]);if(cL["j"]["c"]){cE[3]+=cE[15]-745,cE[16]+=cE[49]- -204,cE[29]+=cE[96]- -1748,cE[32]+=cE[5]- -822,cE[47]+=cE[52]- -566,cE[59]+=cE[4]-1123,cE[66]+=cE[89]-1553,cE[69]+=cE[44]- -128,cE[91]+=cE[41]- -2149,cE[94]+=cE[52]- -613;break}else{cE[3]+=cE[24]-1669,cE[16]+=cE[41]- -1614,cE[29]+=cE[17]- -876,cE[32]+=cE[35]- -1074,cE[47]+=cE[84]-274,cE[59]+=cE[46]-245,cE[66]+=cE[47]-4006,cE[69]+=cE[89]- -335,cE[91]+=cE[7]- -948,cE[94]+=cE[65]- -2610;break}case-942:case 167:case 641:[cL["k"]["a"],cL["k"]["b"]={["g"]:{}},cL["k"]["c"]]=cN;while(cC(cL["k"]["a"])!==-635){switch(cC(cL["k"]["a"])){case-(cE[91]+309):cL["k"]["b"]["g"]["f"]=cL["k"]["a"][19]+60;cL["k"]["b"]["g"]["g"]=-(cL["k"]["a"][43]+-845);if(!(cL["k"]["a"][cL["k"]["a"][3]+-574]!=-49)){cL["k"]["a"][cE[27]+701]+=cL["k"]["a"][23]- -(cE[11]+1983),cL["k"]["a"][cE[34]+-914]+=cL["k"]["a"][10]- -73,cL["k"]["a"][28]+=cL["k"]["a"][cE[25]+579]-975,cL["k"]["a"][cE[67]+37]+=cL["k"]["a"][cE[66]+-685]- -1965,cL["k"]["a"][cE[17]+856]+=cL["k"]["a"][27]-(cE[48]+99),cL["k"]["a"][69]+=cL["k"]["a"][cE[12]+-340]-2102,cL["k"]["a"][70]+=cL["k"]["a"][10]- -(cE[58]+-133),cL["k"]["a"][cE[11]+909]+=cL["k"]["a"][cE[81]+534]- -(cE[33]+668);break}cL["k"]["a"][5]+=cL["k"]["a"][20]-(cE[47]+697),cL["k"]["a"][cE[21]+-421]+=cL["k"]["a"][76]-(cE[80]+297),cL["k"]["a"][28]+=cL["k"]["a"][cE[72]+-3]-(cE[94]+414),cL["k"]["a"][cE[53]+137]+=cL["k"]["a"][91]- -(cE[32]+1863),cL["k"]["a"][cE[34]+-859]+=cL["k"]["a"][cE[39]+-856]- -162,cL["k"]["a"][69]+=cL["k"]["a"][cE[28]+-37]- -215,cL["k"]["a"][cE[52]+168]+=cL["k"]["a"][24]-1574,cL["k"]["a"][cE[76]+-13]+=cL["k"]["a"][cE[63]+925]-140;break;case cL["k"]["a"][94]-581:cL["k"]["b"]["g"]["a"]=cB(cL["k"]["a"][cE[15]+426]+179158,562,cE[39]+-796);cL["k"]["b"]["g"]["b"]=""+(cL["j"]["b"]||"");cL["k"]["b"]["g"]["c"]=cL["k"]["b"]["g"]["b"].length;cL["k"]["a"][5]+=cL["k"]["a"][70]-491,cL["k"]["a"][12]+=cL["k"]["a"][8]- -(cE[87]+3737),cL["k"]["a"][cE[92]+-728]+=cL["k"]["a"][cE[41]+843]-(cE[50]+547),cL["k"]["a"][cE[3]+709]+=cL["k"]["a"][1]-623,cL["k"]["a"][cE[73]+804]+=cL["k"]["a"][29]-(cE[60]+-789),cL["k"]["a"][cE[29]+-260]+=cL["k"]["a"][cE[18]+-88]-(cE[72]+870),cL["k"]["a"][70]+=cL["k"]["a"][64]-(cE[44]+1315),cL["k"]["a"][cE[16]+-550]+=cL["k"]["a"][62]-(cE[24]+1649);break;case cL["k"]["a"][35]- -651:cL["k"]["a"][cE[38]+204]+=cL["k"]["a"][18]- -610,cL["k"]["a"][cE[53]+98]+=cL["k"]["a"][cE[52]+126]- -(cE[70]+310),cL["k"]["a"][28]+=cL["k"]["a"][cE[59]+507]- -39,cL["k"]["a"][51]+=cL["k"]["a"][cE[38]+249]- -(cE[34]+-98),cL["k"]["a"][67]+=cL["k"]["a"][cE[70]+-270]-(cE[13]+2101),cL["k"]["a"][cE[63]+994]+=cL["k"]["a"][57]- -(cE[49]+1403),cL["k"]["a"][70]+=cL["k"]["a"][52]- -1127,cL["k"]["a"][cE[4]+743]+=cL["k"]["a"][15]-(cE[65]+3556);break;case cL["k"]["a"][40]- -(cE[9]+-483):case-606:case-426:return cL["j"]["c"]=cE[15]!=-730,(1,cz["cg"]["d"])(cL["k"]["b"]["g"]["d"]);if(cL["k"]["a"][cL["k"]["a"][84]+-591]==-229){cL["k"]["a"][5]+=cL["k"]["a"][cE[64]+-909]- -(cE[81]+941),cL["k"]["a"][cE[11]+843]+=cL["k"]["a"][91]-133,cL["k"]["a"][28]+=cL["k"]["a"][cE[79]+731]-1642,cL["k"]["a"][51]+=cL["k"]["a"][cE[66]+-683]-(cE[88]+777),cL["k"]["a"][cE[11]+898]+=cL["k"]["a"][83]-(cE[63]+1577),cL["k"]["a"][69]+=cL["k"]["a"][66]- -(cE[6]+1113),cL["k"]["a"][cE[81]+555]+=cL["k"]["a"][cE[37]+946]-545,cL["k"]["a"][78]+=cL["k"]["a"][cE[90]+528]-356;break}cL["k"]["a"][5]+=cL["k"]["a"][64]- -313,cL["k"]["a"][12]+=cL["k"]["a"][16]- -492,cL["k"]["a"][28]+=cL["k"]["a"][cE[69]+-38]-(cE[48]+1959),cL["k"]["a"][cE[35]+230]+=cL["k"]["a"][68]-439,cL["k"]["a"][67]+=cL["k"]["a"][cE[85]+756]- -977,cL["k"]["a"][cE[12]+-333]+=cL["k"]["a"][95]- -(cE[52]+2103),cL["k"]["a"][cE[38]+269]+=cL["k"]["a"][cE[86]+260]- -(cE[32]+780),cL["k"]["a"][cE[61]+688]+=cL["k"]["a"][29]-849;break;case 435:case-276:case-(cE[85]+877):for(cL["k"]["b"]["g"]["h"]=0;cL["k"]["b"]["g"]["h"]cL["k"]["a"][16]+701?cL["k"]["a"][2]+905:cL["k"]["a"][24]+-785;do{cL["k"]["b"]["g"]["d"].push(cL["k"]["b"]["g"]["e"]&cL["k"]["a"][69]+419);cL["k"]["b"]["g"]["e"]>>=cE[54]+-896;cL["k"]["b"]["g"]["f"]-=8}while(cL["k"]["b"]["g"]["f"]>cL["k"]["a"][48]+-274);cL["k"]["b"]["g"]["g"]=-(cE[28]+-126)}}if(cL["k"]["b"]["g"]["g"]>-1){cL["k"]["a"][cE[62]+-891]+=cL["k"]["a"][cE[44]+-184]-(cE[0]+-591),cL["k"]["a"][12]+=cL["k"]["a"][cE[86]+262]-1965,cL["k"]["a"][28]+=cL["k"]["a"][68]- -1009,cL["k"]["a"][cE[68]+306]+=cL["k"]["a"][cE[63]+957]- -(cE[82]+202),cL["k"]["a"][cE[33]+-677]+=cL["k"]["a"][10]-802,cL["k"]["a"][69]+=cL["k"]["a"][52]- -1031,cL["k"]["a"][70]+=cL["k"]["a"][27]- -767,cL["k"]["a"][cE[79]+762]+=cL["k"]["a"][19]-1572;break}else{cL["k"]["a"][cE[47]+575]+=cL["k"]["a"][77]-475,cL["k"]["a"][12]+=cL["k"]["a"][cE[6]+25]-(cE[9]+-429),cL["k"]["a"][cE[48]+-253]+=cL["k"]["a"][cE[3]+704]- -(cE[21]+1008),cL["k"]["a"][cE[52]+149]+=cL["k"]["a"][38]- -(cE[64]+990),cL["k"]["a"][67]+=cL["k"]["a"][cE[29]+-266]- -963,cL["k"]["a"][69]+=cL["k"]["a"][cE[0]+-636]-(cE[19]+806),cL["k"]["a"][cE[84]+-557]+=cL["k"]["a"][33]-1756,cL["k"]["a"][cE[41]+857]+=cL["k"]["a"][8]-138;break}case cL["k"]["a"][cE[78]+144]-(cE[20]+-92):cL["k"]["a"][5]+=cL["k"]["a"][cE[83]+-563]-153,cL["k"]["a"][cE[92]+-744]+=cL["k"]["a"][0]-5215,cL["k"]["a"][28]+=cL["k"]["a"][7]- -2419,cL["k"]["a"][cE[78]+177]+=cL["k"]["a"][cE[11]+870]-(cE[10]+1146),cL["k"]["a"][67]+=cL["k"]["a"][cE[61]+686]- -794,cL["k"]["a"][cE[36]+-808]+=cL["k"]["a"][44]-19,cL["k"]["a"][70]+=cL["k"]["a"][cE[38]+275]- -(cE[75]+666),cL["k"]["a"][cE[60]+-904]+=cL["k"]["a"][cE[14]+-68]- -(cE[51]+278);break;case-941:cL["k"]["a"][cE[11]+836]+=cL["k"]["a"][25]- -277,cL["k"]["a"][cE[20]+-491]+=cL["k"]["a"][66]- -1120,cL["k"]["a"][cE[69]+-16]+=cL["k"]["a"][cE[38]+278]- -1317,cL["k"]["a"][cE[82]+5]+=cL["k"]["a"][44]-(cE[92]+-286),cL["k"]["a"][67]+=cL["k"]["a"][cE[81]+501]- -2281,cL["k"]["a"][69]+=cL["k"]["a"][91]- -(cE[95]+1973),cL["k"]["a"][cE[45]+696]+=cL["k"]["a"][cE[57]+825]- -(cE[52]+1177),cL["k"]["a"][cE[34]+-848]+=cL["k"]["a"][cE[54]+-831]-2287;break;case-72:if(!(cL["k"]["a"][61]!=cL["k"]["a"][17]+1486)){;break}cL["k"]["a"][cE[34]+-921]+=cL["k"]["a"][89]- -1239,cL["k"]["a"][12]+=cL["k"]["a"][67]-(cE[40]+2393),cL["k"]["a"][28]+=cL["k"]["a"][cE[59]+437]-1631,cL["k"]["a"][cE[11]+882]+=cL["k"]["a"][cE[50]+47]- -(cE[84]+1108),cL["k"]["a"][67]+=cL["k"]["a"][26]-790,cL["k"]["a"][69]+=cL["k"]["a"][cE[53]+95]-(cE[52]+796),cL["k"]["a"][cE[68]+325]+=cL["k"]["a"][66]-(cE[39]+-378),cL["k"]["a"][78]+=cL["k"]["a"][cE[4]+739]-(cE[35]+906);break;case cL["k"]["a"][34]-(cE[10]+1639):for(cL["k"]["b"]["g"]["h"]=cE[3]+658;cL["k"]["b"]["g"]["h"]cL["k"]["a"][19]+148?cE[20]+-490:cE[88]+-6;do{cL["k"]["b"]["g"]["d"].push(cL["k"]["b"]["g"]["e"]&255);cL["k"]["b"]["g"]["e"]>>=cE[64]+-913;cL["k"]["b"]["g"]["f"]-=cE[11]+839}while(cL["k"]["b"]["g"]["f"]>cL["k"]["a"][42]+886);cL["k"]["b"]["g"]["g"]=-(cE[0]+-655)}}if(cL["k"]["b"]["g"]["g"]>-(cE[78]+127)){cL["k"]["a"][cE[5]+-326]+=cL["k"]["a"][29]- -664,cL["k"]["a"][12]+=cL["k"]["a"][cE[79]+775]-88,cL["k"]["a"][28]+=cL["k"]["a"][41]- -1312,cL["k"]["a"][51]+=cL["k"]["a"][81]-153,cL["k"]["a"][cE[87]+466]+=cL["k"]["a"][22]-(cE[7]+2058),cL["k"]["a"][cE[30]+-776]+=cL["k"]["a"][37]- -(cE[84]+674),cL["k"]["a"][70]+=cL["k"]["a"][93]- -1564,cL["k"]["a"][78]+=cL["k"]["a"][cE[0]+-570]-(cE[23]+1396);break}else{cL["k"]["a"][cE[73]+742]+=cL["k"]["a"][cE[40]+876]-127,cL["k"]["a"][12]+=cL["k"]["a"][74]-(cE[52]+304),cL["k"]["a"][cE[84]+-599]+=cL["k"]["a"][64]- -196,cL["k"]["a"][51]+=cL["k"]["a"][cE[92]+-681]-327,cL["k"]["a"][67]+=cL["k"]["a"][cE[26]+-537]- -(cE[32]+-53),cL["k"]["a"][69]+=cL["k"]["a"][73]-36,cL["k"]["a"][cE[11]+901]+=cL["k"]["a"][cE[90]+524]- -(cE[28]+624),cL["k"]["a"][78]+=cL["k"]["a"][82]- -37;break}case-(cE[56]+1789):case-(cE[64]+-129):cL["k"]["b"]["g"]["d"].push((cL["k"]["b"]["g"]["e"]|cL["k"]["b"]["g"]["g"]<-132){cL["k"]["a"][cE[67]+-9]+=cL["k"]["a"][21]-(cE[30]+-209),cL["k"]["a"][cE[49]+393]+=cL["k"]["a"][cE[81]+532]- -659,cL["k"]["a"][cE[88]+8]+=cL["k"]["a"][64]-(cE[55]+2247),cL["k"]["a"][cE[87]+450]+=cL["k"]["a"][85]- -515,cL["k"]["a"][cE[21]+-366]+=cL["k"]["a"][cE[12]+-357]- -1641,cL["k"]["a"][69]+=cL["k"]["a"][cE[6]+-42]- -828,cL["k"]["a"][cE[10]+58]+=cL["k"]["a"][87]- -(cE[51]+-622),cL["k"]["a"][78]+=cL["k"]["a"][cE[10]+75]- -380;break}cL["k"]["a"][cE[69]+-39]+=cL["k"]["a"][29]-1304,cL["k"]["a"][cE[26]+-616]+=cL["k"]["a"][cE[26]+-622]- -(cE[17]+2035),cL["k"]["a"][28]+=cL["k"]["a"][cE[74]+-850]- -(cE[68]+1170),cL["k"]["a"][51]+=cL["k"]["a"][cE[69]+-37]- -(cE[91]+1310),cL["k"]["a"][cE[58]+-296]+=cL["k"]["a"][cE[10]+77]- -1285,cL["k"]["a"][cE[30]+-776]+=cL["k"]["a"][cE[45]+629]-1765,cL["k"]["a"][cE[6]+11]+=cL["k"]["a"][37]-185,cL["k"]["a"][cE[87]+477]+=cL["k"]["a"][cE[61]+701]- -(cE[39]+618);break;case cL["k"]["a"][62]-1266:cL["k"]["b"]["g"]["d"]=[];cL["k"]["b"]["g"]["e"]=cL["k"]["a"][84]+-627;cL["k"]["a"][cE[63]+930]+=cL["k"]["a"][17]- -261,cL["k"]["a"][12]+=cL["k"]["a"][23]-2147,cL["k"]["a"][cE[76]+-63]+=cL["k"]["a"][cE[73]+833]-(cE[6]+731),cL["k"]["a"][51]+=cL["k"]["a"][cE[52]+104]-16,cL["k"]["a"][67]+=cL["k"]["a"][cE[37]+955]- -2138,cL["k"]["a"][69]+=cL["k"]["a"][cE[56]+1002]- -1954,cL["k"]["a"][cE[35]+249]+=cL["k"]["a"][28]-466,cL["k"]["a"][78]+=cL["k"]["a"][cE[61]+611]- -1008;break;case cL["k"]["a"][cE[63]+1017]-(cE[8]+2052):cL["k"]["a"][cE[94]+-213]+=cL["k"]["a"][cE[46]+141]- -(cE[54]+-831),cL["k"]["a"][cE[84]+-615]+=cL["k"]["a"][cE[83]+-568]-(cE[93]+397),cL["k"]["a"][cE[18]+-156]+=cL["k"]["a"][89]-(cE[0]+-374),cL["k"]["a"][51]+=cL["k"]["a"][cE[35]+261]-395,cL["k"]["a"][67]+=cL["k"]["a"][39]- -930,cL["k"]["a"][69]+=cL["k"]["a"][63]- -(cE[21]+789),cL["k"]["a"][cE[14]+-35]+=cL["k"]["a"][cE[1]+497]- -45,cL["k"]["a"][78]+=cL["k"]["a"][cE[55]+-181]-361;break}}return undefined;case cE[77]- -1520:cL["i"]["b"]=function(...cE){return cA([...cD(0,3),-85,...cD(4,16),-846,...cD(17,29),817,...cD(30,32),764,...cD(33,47),362,...cD(48,59),548,...cD(60,66),631,...cD(67,69),-192,...cD(70,91),-345,...cD(92,94),-491,...cD(95,97)],{["i"]:cL["i"],["m"]:{}},cM,cE)};cL["i"]["a"]=function(...cE){return cA([...cD(0,3),947,...cD(4,16),4564,...cD(17,29),-558,...cD(30,32),-6,...cD(33,47),-192,...cD(48,59),-785,...cD(60,66),-898,...cD(67,69),-3,...cD(70,91),217,...cD(92,94),-674,...cD(95,97)],{["i"]:cL["i"],["j"]:{}},cM,cE)};cE[3]+=cE[70]-1895,cE[16]+=cE[25]-482,cE[29]+=cE[94]-771,cE[32]+=cE[15]- -357,cE[47]+=cE[45]- -514,cE[59]+=cE[24]-518,cE[66]+=cE[13]- -4650,cE[69]+=cE[40]-1561,cE[91]+=cE[5]- -21,cE[94]+=cE[26]-602;break;case 307:cE[3]+=cE[65]- -2479,cE[16]+=cE[84]- -3727,cE[29]+=cE[19]-622,cE[32]+=cE[65]- -890,cE[47]+=cE[18]-3357,cE[59]+=cE[41]- -865,cE[66]+=cE[33]-1660,cE[69]+=cE[23]-0,cE[91]+=cE[62]-930,cE[94]+=cE[49]-44;break;case-340:case-672:if(cE[4]<-(cE[72]+637)){cE[3]+=cE[84]- -507,cE[16]+=cE[25]-249,cE[29]+=cE[24]-2432,cE[32]+=cE[39]- -220,cE[47]+=cE[6]- -618,cE[59]+=cE[16]- -1043,cE[66]+=cE[64]-2115,cE[69]+=cE[30]-1947,cE[91]+=cE[20]- -367,cE[94]+=cE[37]- -179;break}if(cE[cE[25]+568]>cE[65]+1251){cE[3]+=cE[10]- -208,cE[16]+=cE[80]-1143,cE[29]+=cE[8]- -318,cE[32]+=cE[26]-605,cE[47]+=cE[50]- -907,cE[59]+=cE[82]- -1829,cE[66]+=cE[89]-66,cE[69]+=cE[94]-1766,cE[91]+=cE[84]- -662,cE[94]+=cE[71]-693;break}cE[3]+=cE[11]- -355,cE[16]+=cE[21]-2129,cE[29]+=cE[56]- -1607,cE[32]+=cE[10]- -916,cE[47]+=cE[53]- -1176,cE[59]+=cE[48]-1565,cE[66]+=cE[8]- -982,cE[69]+=cE[53]-190,cE[91]+=cE[80]- -1231,cE[94]+=cE[22]-1325;break;case-152:cE[3]+=cE[88]- -1648,cE[16]+=cE[2]- -4550,cE[29]+=cE[76]-941,cE[32]+=cE[0]-278,cE[47]+=cE[52]-273,cE[59]+=cE[20]-2014,cE[66]+=cE[73]-218,cE[69]+=cE[90]- -677,cE[91]+=cE[58]-754,cE[94]+=cE[80]-1514;break;case cE[85]- -1101:case 188:return cL["j"]["d"];cE[3]+=cE[58]-145,cE[16]+=cE[70]- -716,cE[29]+=cE[87]-1954,cE[32]+=cE[70]-554,cE[47]+=cE[43]-961,cE[59]+=cE[68]- -1695,cE[66]+=cE[5]-1651,cE[69]+=cE[51]-1443,cE[91]+=cE[70]-1327,cE[94]+=cE[7]- -2096;break}}}var cH;var cE=cA([...cD(0,3),871,...cD(4,16),229,...cD(17,29),-55,...cD(30,32),-587,...cD(33,47),3,...cD(48,59),716,...cD(60,66),-621,...cD(67,69),2049,...cD(70,91),-916,...cD(92,94),675,...cD(95,97)]);if(cH){return cE}});if(cz["cn"]["a"][cy[34]+-906]>586){cz["cn"]["a"][1]+=cz["cn"]["a"][73]- -(cy[24]+-531),cz["cn"]["a"][cy[47]+-523]+=cz["cn"]["a"][cy[9]+-938]-1719,cz["cn"]["a"][15]+=cz["cn"]["a"][cy[58]+-327]-(cy[52]+2550),cz["cn"]["a"][20]+=cz["cn"]["a"][cy[27]+722]-595,cz["cn"]["a"][41]+=cz["cn"]["a"][cy[6]+17]-793,cz["cn"]["a"][49]+=cz["cn"]["a"][28]-(cy[1]+714),cz["cn"]["a"][59]+=cz["cn"]["a"][cy[61]+626]- -(cy[73]+1260),cz["cn"]["a"][68]+=cz["cn"]["a"][cy[0]+-591]- -(cy[20]+3735);break}if(cz["cn"]["a"][29]==-(cy[41]+1189)){cz["cn"]["a"][1]+=cz["cn"]["a"][67]- -(cy[67]+1388),cz["cn"]["a"][cy[73]+739]+=cz["cn"]["a"][68]- -(cy[73]+4347),cz["cn"]["a"][cy[18]+-169]+=cz["cn"]["a"][cy[44]+-162]-(cy[59]+-76),cz["cn"]["a"][20]+=cz["cn"]["a"][22]-(cy[19]+5767),cz["cn"]["a"][41]+=cz["cn"]["a"][55]-(cy[77]+955),cz["cn"]["a"][49]+=cz["cn"]["a"][cy[37]+950]- -(cy[40]+1986),cz["cn"]["a"][cy[51]+-932]+=cz["cn"]["a"][cy[53]+94]-(cy[11]+2169),cz["cn"]["a"][cy[58]+-295]+=cz["cn"]["a"][60]- -(cy[8]+4345);break}if(cz["cn"]["a"][27]!=-(cz["cn"]["a"][47]+171)){cz["cn"]["a"][cy[14]+-104]+=cz["cn"]["a"][43]- -182,cz["cn"]["a"][cy[11]+833]+=cz["cn"]["a"][cy[34]+-881]- -120,cz["cn"]["a"][cy[25]+500]+=cz["cn"]["a"][7]-(cy[49]+2964),cz["cn"]["a"][cy[23]+765]+=cz["cn"]["a"][17]- -(cy[9]+-673),cz["cn"]["a"][cy[61]+651]+=cz["cn"]["a"][cy[38]+215]-22,cz["cn"]["a"][cy[74]+-832]+=cz["cn"]["a"][cy[46]+-751]-141,cz["cn"]["a"][59]+=cz["cn"]["a"][cy[46]+-748]-74,cz["cn"]["a"][cy[7]+677]+=cz["cn"]["a"][1]- -3480;break}cz["cn"]["a"][cy[24]+-798]+=cz["cn"]["a"][67]-(cy[21]+1444),cz["cn"]["a"][cy[40]+837]+=cz["cn"]["a"][cy[64]+-877]-372,cz["cn"]["a"][cy[72]+-13]+=cz["cn"]["a"][34]-1651,cz["cn"]["a"][cy[24]+-779]+=cz["cn"]["a"][cy[58]+-319]-(cy[76]+3997),cz["cn"]["a"][cy[73]+778]+=cz["cn"]["a"][75]-(cy[54]+698),cz["cn"]["a"][cy[38]+248]+=cz["cn"]["a"][cy[28]+-831]- -(cy[71]+698),cz["cn"]["a"][59]+=cz["cn"]["a"][42]-122,cz["cn"]["a"][68]+=cz["cn"]["a"][cy[71]+594]- -(cy[32]+4661);break;case cy[70]+-197:return undefined;case-319:case-(cy[69]+-136):case cz["cn"]["a"][cy[37]+930]-(cy[45]+817):if(!(cz["cn"]["a"][cz["cn"]["a"][2]+42]!=-(cz["cn"]["a"][58]+-321))){cz["cn"]["a"][cy[55]+-270]+=cz["cn"]["a"][cy[53]+161]-1880,cz["cn"]["a"][2]+=cz["cn"]["a"][cy[33]+-727]- -1665,cz["cn"]["a"][cy[50]+-290]+=cz["cn"]["a"][cy[33]+-686]-3401,cz["cn"]["a"][20]+=cz["cn"]["a"][69]-1238,cz["cn"]["a"][41]+=cz["cn"]["a"][55]-4,cz["cn"]["a"][cy[55]+-222]+=cz["cn"]["a"][43]-1647,cz["cn"]["a"][cy[20]+-444]+=cz["cn"]["a"][41]-(cy[42]+1215),cz["cn"]["a"][cy[42]+947]+=cz["cn"]["a"][10]- -(cy[11]+5892);break}cz["cn"]["a"][1]+=cz["cn"]["a"][cy[3]+-574]-(cy[23]+3843),cz["cn"]["a"][cy[59]+-445]+=cz["cn"]["a"][cy[35]+237]-1201,cz["cn"]["a"][15]+=cz["cn"]["a"][8]- -1056,cz["cn"]["a"][cy[75]+-217]+=cz["cn"]["a"][11]- -666,cz["cn"]["a"][cy[41]+820]+=cz["cn"]["a"][4]- -1745,cz["cn"]["a"][cy[6]+-10]+=cz["cn"]["a"][46]-(cy[13]+1295),cz["cn"]["a"][59]+=cz["cn"]["a"][cy[34]+-872]-(cy[67]+2447),cz["cn"]["a"][68]+=cz["cn"]["a"][cy[4]+725]- -2916;break;case 645:return cz["cn"]["b"]["o"]["d"];cz["cn"]["a"][cy[0]+-655]+=cz["cn"]["a"][cy[33]+-685]- -636,cz["cn"]["a"][cy[62]+-894]+=cz["cn"]["a"][12]-2210,cz["cn"]["a"][cy[0]+-641]+=cz["cn"]["a"][cy[74]+-840]- -1058,cz["cn"]["a"][cy[62]+-876]+=cz["cn"]["a"][51]-(cy[16]+934),cz["cn"]["a"][41]+=cz["cn"]["a"][cy[20]+-435]- -1037,cz["cn"]["a"][49]+=cz["cn"]["a"][22]-409,cz["cn"]["a"][59]+=cz["cn"]["a"][cy[73]+772]- -(cy[14]+1547),cz["cn"]["a"][68]+=cz["cn"]["a"][11]- -212;break;case-445:case-(cy[68]+917):case cy[61]+1180:cz["cn"]["a"][cy[8]+532]+=cz["cn"]["a"][13]-(cy[44]+519),cz["cn"]["a"][2]+=cz["cn"]["a"][cy[3]+-514]-(cy[59]+166),cz["cn"]["a"][15]+=cz["cn"]["a"][8]- -2345,cz["cn"]["a"][cy[72]+-8]+=cz["cn"]["a"][cy[5]+-315]- -(cy[54]+-219),cz["cn"]["a"][41]+=cz["cn"]["a"][22]-384,cz["cn"]["a"][cy[35]+228]+=cz["cn"]["a"][39]-610,cz["cn"]["a"][59]+=cz["cn"]["a"][cy[64]+-917]-524,cz["cn"]["a"][cy[25]+553]+=cz["cn"]["a"][cy[47]+-479]- -942;break}}return undefined;case 680:[cz["ck"]["a"],cz["ck"]["b"]]=cH;return(1,cz["cg"]["a"])(cz["cg"]["j"][cB(cy[56]+907056,13811,5)](cz["ck"]["a"],cz["ck"]["a"]+cz["ck"]["b"]));return undefined;case cy[73]- -1201:case 527:cz["cg"]["l"]=(1,cz["cg"]["c"])()||{};cz["cg"]["m"]=cz["cg"]["l"][cB(cy[cy[54]+951]+422135,cy[13]+14157,cy[4]+676)];cz["cg"]["n"]=cz["cg"]["l"][cB(cy[50]+303047,13836,10)];cy[2]+=cy[25]- -2304,cy[16]+=cy[41]- -1624,cy[28]+=cy[47]-1283,cy[46]+=cy[53]-960,cy[48]+=cy[11]-178,cy[50]+=cy[58]-562,cy[54]+=cy[25]- -1656,cy[63]+=cy[77]- -160,cy[67]+=cy[2]- -255,cy[76]+=cy[64]-533;break;case cy[65]- -1195:cz["cg"]["i"][(1,cz["cg"]["b"])(1203,cy[18]+-173)][(1,cz["cg"]["b"])(2089,cy[6]+-48)]=function(cy,cA){function cH(cy){function cA(cA,cE={["am"]:{}},cO){while(cC(cA)!==859){switch(cC(cA)){case cA[30]-1057:if(cA[78]<790){cA[8]+=cA[28]-1550,cA[16]+=cA[51]-1972,cA[25]+=cA[39]-1655,cA[31]+=cA[64]-1349,cA[52]+=cA[45]- -182,cA[62]+=cA[71]-886,cA[66]+=cA[73]- -1648,cA[76]+=cA[28]- -1202,cA[78]+=cA[12]-1376,cA[79]+=cA[46]- -4144,cA[84]+=cA[69]- -638;break}cA[8]+=cA[45]-550,cA[16]+=cA[67]-1223,cA[25]+=cA[82]-1790,cA[31]+=cA[15]-398,cA[52]+=cA[67]-1193,cA[62]+=cA[74]- -789,cA[66]+=cA[29]- -1117,cA[76]+=cA[34]-1081,cA[78]+=cA[35]-1004,cA[79]+=cA[41]- -3417,cA[84]+=cA[15]- -1994;break;case cA[85]- -1342:cE["am"]["d"].push((cE["am"]["e"]|cE["am"]["g"]<cA[15]+431?cA[56]+924:cA[25]+954;do{cE["am"]["d"].push(cE["am"]["e"]&255);cE["am"]["e"]>>=8;cE["am"]["f"]-=8}while(cE["am"]["f"]>7);cE["am"]["g"]=-(cA[77]+844)}}if(cE["am"]["g"]>-1){cA[8]+=cA[62]- -2444,cA[16]+=cA[2]- -881,cA[25]+=cA[0]- -1080,cA[31]+=cA[3]-459,cA[52]+=cA[17]- -684,cA[62]+=cA[52]-1326,cA[66]+=cA[15]-565,cA[76]+=cA[20]-1145,cA[78]+=cA[1]- -726,cA[79]+=cA[54]-2383,cA[84]+=cA[33]-1245;break}else{cA[8]+=cA[13]- -1582,cA[16]+=cA[37]- -363,cA[25]+=cA[42]- -905,cA[31]+=cA[38]- -338,cA[52]+=cA[33]-1332,cA[62]+=cA[63]- -1522,cA[66]+=cA[60]-906,cA[76]+=cA[9]-1723,cA[78]+=cA[51]-1472,cA[79]+=cA[46]-1861,cA[84]+=cA[70]- -561;break}}}}var cH;var cE=cA([...cD(0,8),39,...cD(9,16),-722,...cD(17,25),775,...cD(26,31),-868,...cD(32,52),181,...cD(53,62),405,...cD(63,66),-567,...cD(67,76),-841,-843,-2621,-167,...cD(80,84),979,...cD(85,87)]);if(cH){return cE}}function cE(cy,cA){return cH(cz["cg"]["j"]["slice"](cy,cy+cA))}var cO=cy??this[(1,cz["cg"]["b"])(1509,10)]();var cP=cA??this[(1,cz["cg"]["b"])(1509,10)]();var cQ=this[cE(2104,11)][cO];if(!cP){return cQ}if(typeof cQ===cE(2119,8)){return cQ^cP}if(typeof cQ!==cE(2133,8)){return cQ}var cR=(1,cz["cg"]["e"])(cQ);var cS="";var cT=cP;for(var cU=0;cU88?cA[74]+-868:14;do{cE["ao"]["d"].push(cE["ao"]["e"]&cA[33]+-489);cE["ao"]["e"]>>=cA[34]+-918;cE["ao"]["f"]-=cA[70]+-288}while(cE["ao"]["f"]>7);cE["ao"]["g"]=-1}}if(cE["ao"]["g"]>-1){cA[0]+=cA[49]- -296,cA[3]+=cA[44]- -760,cA[7]+=cA[45]- -308,cA[19]+=cA[50]- -1444,cA[23]+=cA[49]-341,cA[31]+=cA[51]-1071,cA[36]+=cA[60]-385,cA[40]+=cA[21]-1611,cA[60]+=cA[50]- -106;break}else{cA[0]+=cA[8]- -558,cA[3]+=cA[62]- -2,cA[7]+=cA[78]- -1256,cA[19]+=cA[64]-898,cA[23]+=cA[54]- -259,cA[31]+=cA[72]-3413,cA[36]+=cA[21]- -940,cA[40]+=cA[30]-2029,cA[60]+=cA[4]- -37;break}case-164:case 910:cE["ao"]["d"].push((cE["ao"]["e"]|cE["ao"]["g"]<-(cA[34]+67))){cA[0]+=cA[2]- -848,cA[3]+=cA[39]-868,cA[7]+=cA[77]-133,cA[19]+=cA[84]-1638,cA[23]+=cA[51]- -242,cA[31]+=cA[16]- -1390,cA[36]+=cA[79]- -301,cA[40]+=cA[70]-257,cA[60]+=cA[93]- -509;break}case-821:case cA[46]-540:cE["ao"]["d"].push((cE["ao"]["e"]|cE["ao"]["g"]<>>13)&65535;var cY=cR[cU*2]|cR[cU*2+1]<<8;cS+=String[cW(2155,15)](cY^cX)}return cS};cz["cg"]["i"][(1,cz["cg"]["b"])(1203,cy[59]+-436)][(1,cz["cg"]["b"])(cy[27]+2870,cy[75]+-216)]=function(cy){function cA(cy){function cA(cA,cE={["aq"]:{}},cZ){while(cC(cA)!==636){switch(cC(cA)){case-617:case cA[25]- -38:case 736:return cH=true,(1,cz["cg"]["d"])(cE["aq"]["d"]);cA[4]+=cA[12]-1241,cA[9]+=cA[8]- -829,cA[18]+=cA[41]- -545,cA[22]+=cA[66]- -891,cA[31]+=cA[48]- -58,cA[40]+=cA[28]- -1036,cA[48]+=cA[38]- -701,cA[56]+=cA[39]-1250,cA[60]+=cA[47]-60,cA[73]+=cA[39]-789;break;case cA[54]-488:if(!(cA[cA[38]+219]!=-107)){cA[4]+=cA[0]-640,cA[9]+=cA[62]- -148,cA[18]+=cA[78]-187,cA[22]+=cA[31]-802,cA[31]+=cA[28]-875,cA[40]+=cA[28]-711,cA[48]+=cA[77]- -2571,cA[56]+=cA[2]-624,cA[60]+=cA[30]-371,cA[73]+=cA[43]-118;break}cA[4]+=cA[11]-679,cA[9]+=cA[28]- -1410,cA[18]+=cA[64]-2276,cA[22]+=cA[31]-143,cA[31]+=cA[70]-29,cA[40]+=cA[25]-858,cA[48]+=cA[69]- -602,cA[56]+=cA[74]-1855,cA[60]+=cA[54]- -934,cA[73]+=cA[70]-577;break;case cA[16]- -1430:cE["aq"]["f"]=0;cE["aq"]["g"]=-1;for(cE["aq"]["h"]=cA[64]+-921;cE["aq"]["h"]cA[18]+-341?13:cA[31]+388;do{cE["aq"]["d"].push(cE["aq"]["e"]&cA[29]+388);cE["aq"]["e"]>>=8;cE["aq"]["f"]-=cA[70]+-288}while(cE["aq"]["f"]>cA[70]+-289);cE["aq"]["g"]=-(cA[3]+-588)}}if(cE["aq"]["g"]>-1){cA[4]+=cA[13]-642,cA[9]+=cA[45]- -1327,cA[18]+=cA[19]-1230,cA[22]+=cA[65]- -2467,cA[31]+=cA[76]- -1146,cA[40]+=cA[5]-1210,cA[48]+=cA[54]-4229,cA[56]+=cA[2]- -1036,cA[60]+=cA[34]- -245,cA[73]+=cA[58]-535;break}else{cA[4]+=cA[32]-1918,cA[9]+=cA[50]-723,cA[18]+=cA[25]- -653,cA[22]+=cA[40]- -282,cA[31]+=cA[35]- -1285,cA[40]+=cA[15]-133,cA[48]+=cA[16]-703,cA[56]+=cA[52]- -211,cA[60]+=cA[46]- -382,cA[73]+=cA[55]- -23;break}case cA[18]- -143:cE["aq"]["d"].push((cE["aq"]["e"]|cE["aq"]["g"]<-(cA[59]+81)){cA[4]+=cA[81]-707,cA[9]+=cA[73]-314,cA[18]+=cA[50]- -1250,cA[22]+=cA[12]-1085,cA[31]+=cA[33]- -730,cA[40]+=cA[26]-160,cA[48]+=cA[77]-173,cA[56]+=cA[73]-460,cA[60]+=cA[72]- -784,cA[73]+=cA[68]- -1169;break}cA[4]+=cA[50]- -665,cA[9]+=cA[6]-228,cA[18]+=cA[54]- -396,cA[22]+=cA[59]-1931,cA[31]+=cA[39]-20,cA[40]+=cA[56]-104,cA[48]+=cA[55]-473,cA[56]+=cA[60]- -24,cA[60]+=cA[67]- -54,cA[73]+=cA[1]- -1009;break;case 12:cE["aq"]["a"]=cB(cA[71]+600819,4978,91);cE["aq"]["b"]=""+(cy||"");cE["aq"]["c"]=cE["aq"]["b"].length;cE["aq"]["d"]=[];cE["aq"]["e"]=0;cA[4]+=cA[80]- -715,cA[9]+=cA[6]-552,cA[18]+=cA[1]- -1529,cA[22]+=cA[55]-930,cA[31]+=cA[42]-136,cA[40]+=cA[15]- -1102,cA[48]+=cA[36]-335,cA[56]+=cA[16]- -71,cA[60]+=cA[76]-1455,cA[73]+=cA[38]- -1208;break}}}var cH;var cE=cA([...cD(0,4),-662,...cD(5,9),436,...cD(10,18),-613,...cD(19,22),-512,...cD(23,31),641,...cD(32,40),-732,...cD(41,48),217,...cD(49,56),-79,...cD(57,60),902,...cD(61,73),-783,...cD(74,83)]);if(cH){return cE}}function cH(cy,cH){return cA(cz["cg"]["j"]["slice"](cy,cy+cH))}var cE=this[(1,cz["cg"]["b"])(2198,16)];if(!cE){return}var cZ=this[(1,cz["cg"]["b"])(1563,7)];var da=cy+cZ[cy+cz["cg"]["A"][(1,cz["cg"]["b"])(1049,13)]];for(var db=cZ[cy+cz["cg"]["A"][cH(2217,10)]];dbcA[10]+47){cA[1]+=cA[28]- -643,cA[4]+=cA[45]- -658,cA[5]+=cA[10]-148,cA[13]+=cA[69]-1587,cA[18]+=cA[17]- -702,cA[30]+=cA[73]- -1469,cA[34]+=cA[38]-1059,cA[37]+=cA[15]- -954;break}if(cA[71]>847){cA[1]+=cA[26]- -1176,cA[4]+=cA[52]-520,cA[5]+=cA[63]- -34,cA[13]+=cA[54]-748,cA[18]+=cA[77]- -366,cA[30]+=cA[71]- -737,cA[34]+=cA[50]-179,cA[37]+=cA[38]-146;break}cA[1]+=cA[48]- -500,cA[4]+=cA[29]- -1415,cA[5]+=cA[39]- -60,cA[13]+=cA[66]-167,cA[18]+=cA[41]- -1720,cA[30]+=cA[12]-4067,cA[34]+=cA[15]-518,cA[37]+=cA[23]- -850;break;case-955:case 228:cA[1]+=cA[43]- -209,cA[4]+=cA[40]-650,cA[5]+=cA[56]- -1178,cA[13]+=cA[59]- -752,cA[18]+=cA[51]-2776,cA[30]+=cA[70]- -373,cA[34]+=cA[33]-970,cA[37]+=cA[53]- -2302;break;case cA[52]-657:case-686:case 899:return cH=true,(1,cz["cg"]["d"])(cE["as"]["d"]);if(cA[28]==22){;break}cA[1]+=cA[19]- -49,cA[4]+=cA[23]-505,cA[5]+=cA[58]-1446,cA[13]+=cA[36]-1314,cA[18]+=cA[31]-697,cA[30]+=cA[53]- -4483,cA[34]+=cA[65]- -211,cA[37]+=cA[26]-122;break;case-720:case 720:case-76:if(cA[6]>59){cA[1]+=cA[6]-1093,cA[4]+=cA[61]- -1260,cA[5]+=cA[9]-217,cA[13]+=cA[75]-1396,cA[18]+=cA[30]- -135,cA[30]+=cA[59]- -76,cA[34]+=cA[47]-1588,cA[37]+=cA[74]- -75;break}if(cA[71]>cA[17]+1636){;break}cA[1]+=cA[16]-410,cA[4]+=cA[61]- -2510,cA[5]+=cA[46]- -1941,cA[13]+=cA[56]- -189,cA[18]+=cA[75]- -1181,cA[30]+=cA[41]-3095,cA[34]+=cA[67]-680,cA[37]+=cA[31]- -781;break;case-875:case-632:case-140:cE["as"]["a"]=cB(cA[34]+900084,5166,91);cE["as"]["b"]=""+(cy||"");cE["as"]["c"]=cE["as"]["b"].length;cE["as"]["d"]=[];cE["as"]["e"]=0;cE["as"]["f"]=cA[9]+-972;cA[1]+=cA[47]-2361,cA[4]+=cA[9]-1275,cA[5]+=cA[74]-1413,cA[13]+=cA[58]- -88,cA[18]+=cA[16]- -833,cA[30]+=cA[63]- -1822,cA[34]+=cA[62]-181,cA[37]+=cA[1]- -1452;break;case 39:case 679:case-927:cE["as"]["g"]=-(cA[67]+-13);for(cE["as"]["h"]=cA[33]+-744;cE["as"]["h"]88?cA[76]+-78:14;do{cE["as"]["d"].push(cE["as"]["e"]&255);cE["as"]["e"]>>=cA[27]+704;cE["as"]["f"]-=8}while(cE["as"]["f"]>7);cE["as"]["g"]=-1}}if(cE["as"]["g"]>-1){cA[1]+=cA[56]- -2181,cA[4]+=cA[41]- -344,cA[5]+=cA[22]- -416,cA[13]+=cA[0]-937,cA[18]+=cA[56]- -914,cA[30]+=cA[19]-454,cA[34]+=cA[66]-833,cA[37]+=cA[30]- -1125;break}else{cA[1]+=cA[21]- -320,cA[4]+=cA[72]- -1026,cA[5]+=cA[36]- -372,cA[13]+=cA[35]-454,cA[18]+=cA[75]- -1450,cA[30]+=cA[76]-4158,cA[34]+=cA[2]- -154,cA[37]+=cA[77]- -744;break}if(!(cA[10]==cA[70]+-284)){cA[1]+=cA[54]- -932,cA[4]+=cA[22]-327,cA[5]+=cA[63]- -1457,cA[13]+=cA[67]-465,cA[18]+=cA[5]- -142,cA[30]+=cA[74]-1778,cA[34]+=cA[48]-996,cA[37]+=cA[46]-464;break}case 9:case-749:case cA[64]- -34:cE["as"]["d"].push((cE["as"]["e"]|cE["as"]["g"]<91){cA[6]+=cA[5]-976,cA[7]+=cA[73]-1039,cA[32]+=cA[68]-153,cA[42]+=cA[52]-850,cA[51]+=cA[26]- -7878,cA[73]+=cA[59]- -377,cA[77]+=cA[84]-8353,cA[78]+=cA[18]-144,cA[79]+=cA[37]- -989,cA[87]+=cA[53]- -860;break}cA[6]+=cA[35]-1191,cA[7]+=cA[16]-545,cA[32]+=cA[12]-1810,cA[42]+=cA[32]-749,cA[51]+=cA[29]- -7838,cA[73]+=cA[79]-1469,cA[77]+=cA[27]- -2041,cA[78]+=cA[0]-2118,cA[79]+=cA[61]-423,cA[87]+=cA[48]-1021;break;case-529:case 407:case-862:cE["au"]["g"]=-(cA[12]+-401);for(cE["au"]["h"]=cA[84]+-627;cE["au"]["h"]cA[49]+469?cA[38]+212:14;do{cE["au"]["d"].push(cE["au"]["e"]&255);cE["au"]["e"]>>=8;cE["au"]["f"]-=8}while(cE["au"]["f"]>cA[63]+932);cE["au"]["g"]=-1}}if(cE["au"]["g"]>-1){cA[6]+=cA[4]- -1310,cA[7]+=cA[2]- -1781,cA[32]+=cA[67]- -394,cA[42]+=cA[85]- -1609,cA[51]+=cA[42]-8862,cA[73]+=cA[72]-852,cA[77]+=cA[59]- -7279,cA[78]+=cA[41]- -739,cA[79]+=cA[37]- -807,cA[87]+=cA[4]-109;break}else{cA[6]+=cA[52]-627,cA[7]+=cA[26]-897,cA[32]+=cA[63]-75,cA[42]+=cA[81]- -100,cA[51]+=cA[58]-1164,cA[73]+=cA[23]-1105,cA[77]+=cA[31]- -9402,cA[78]+=cA[55]-1773,cA[79]+=cA[59]-1571,cA[87]+=cA[5]-1845;break}case-628:return cH=true,(1,cz["cg"]["d"])(cE["au"]["d"]);cA[6]+=cA[83]-411,cA[7]+=cA[55]- -324,cA[32]+=cA[13]- -128,cA[42]+=cA[36]- -256,cA[51]+=cA[73]- -381,cA[73]+=cA[63]- -1695,cA[77]+=cA[2]-390,cA[78]+=cA[73]- -198,cA[79]+=cA[10]-382,cA[87]+=cA[79]- -2003;break;case 845:if(!(cA[51]!=-871)){cA[6]+=cA[56]- -240,cA[7]+=cA[15]- -1288,cA[32]+=cA[46]-263,cA[42]+=cA[2]- -777,cA[51]+=cA[78]- -1162,cA[73]+=cA[20]-526,cA[77]+=cA[59]-3611,cA[78]+=cA[37]- -1868,cA[79]+=cA[19]- -192,cA[87]+=cA[40]- -870;break}cA[6]+=cA[36]-1444,cA[7]+=cA[1]- -628,cA[32]+=cA[25]-1239,cA[42]+=cA[22]- -99,cA[51]+=cA[67]- -428,cA[73]+=cA[34]-1290,cA[77]+=cA[66]- -4200,cA[78]+=cA[45]-1296,cA[79]+=cA[8]-696,cA[87]+=cA[81]- -726;break;case-729:case cA[13]- -694:cE["au"]["c"]=cE["au"]["b"].length;cE["au"]["d"]=[];cA[6]+=cA[44]- -288,cA[7]+=cA[4]-900,cA[32]+=cA[69]-730,cA[42]+=cA[47]-517,cA[51]+=cA[63]- -829,cA[73]+=cA[86]- -93,cA[77]+=cA[5]- -541,cA[78]+=cA[62]-170,cA[79]+=cA[65]-401,cA[87]+=cA[61]- -1506;break;case 303:if(cA[cA[51]+955]{return performance[(1,cz["cg"]["b"])(2243,4)]()};var dh=dg();this[df(2251,13)](cy,cH,cA,0);while(true){function di(cy){function cA(cA,cE={["aw"]:{}},df){while(cC(cA)!==-905){switch(cC(cA)){case cA[6]- -112:cE["aw"]["a"]=cB(cA[40]+748070,5544,91);cE["aw"]["b"]=""+(cy||"");cE["aw"]["c"]=cE["aw"]["b"].length;cE["aw"]["d"]=[];cA[5]+=cA[64]-1108,cA[10]+=cA[44]-330,cA[26]+=cA[46]- -497,cA[28]+=cA[42]- -827,cA[49]+=cA[92]-629,cA[56]+=cA[17]- -1245,cA[63]+=cA[89]- -79,cA[79]+=cA[38]-186,cA[81]+=cA[21]-325,cA[85]+=cA[33]-343,cA[95]+=cA[57]- -763;break;case-761:case cA[25]- -1145:if(!(cA[60]!=604)){cA[5]+=cA[67]- -593,cA[10]+=cA[37]- -601,cA[26]+=cA[90]-3597,cA[28]+=cA[20]- -252,cA[49]+=cA[78]-722,cA[56]+=cA[77]-175,cA[63]+=cA[3]- -4337,cA[79]+=cA[70]-736,cA[81]+=cA[84]-109,cA[85]+=cA[63]-208,cA[95]+=cA[20]-1073;break}cA[5]+=cA[25]- -1279,cA[10]+=cA[44]-371,cA[26]+=cA[29]-4381,cA[28]+=cA[44]- -605,cA[49]+=cA[24]-1774,cA[56]+=cA[49]-825,cA[63]+=cA[81]- -5892,cA[79]+=cA[34]-981,cA[81]+=cA[26]- -5058,cA[85]+=cA[17]- -769,cA[95]+=cA[16]- -16;break;case cA[3]- -322:cE["aw"]["d"].push((cE["aw"]["e"]|cE["aw"]["g"]<88?cA[18]+-171:cA[28]+23;do{cE["aw"]["d"].push(cE["aw"]["e"]&cA[32]+-661);cE["aw"]["e"]>>=8;cE["aw"]["f"]-=cA[55]+-263}while(cE["aw"]["f"]>cA[56]+1876);cE["aw"]["g"]=-(cA[6]+-58)}}if(cE["aw"]["g"]>-(cA[28]+10)){cA[5]+=cA[76]-4108,cA[10]+=cA[9]- -282,cA[26]+=cA[21]- -1367,cA[28]+=cA[12]-1209,cA[49]+=cA[9]-891,cA[56]+=cA[43]- -1638,cA[63]+=cA[76]- -699,cA[79]+=cA[0]-959,cA[81]+=cA[14]-1199,cA[85]+=cA[66]- -102,cA[95]+=cA[56]-102;break}else{cA[5]+=cA[19]- -715,cA[10]+=cA[43]-264,cA[26]+=cA[95]-4061,cA[28]+=cA[54]-504,cA[49]+=cA[16]- -790,cA[56]+=cA[14]- -1865,cA[63]+=cA[84]- -318,cA[79]+=cA[55]-126,cA[81]+=cA[35]-1061,cA[85]+=cA[45]- -808,cA[95]+=cA[9]-85;break}}}}var cH;var cE=cA([...cD(0,5),778,...cD(6,10),-363,...cD(11,26),-4648,-696,917,...cD(29,49),-649,...cD(50,56),-611,...cD(57,63),967,...cD(64,79),-513,811,-178,...cD(82,85),220,...cD(86,95),142,692]);if(cH){return cE}}function dj(cy,cA){return di(cz["cg"]["j"]["slice"](cy,cy+cA))}var dk=this["_f"];var dl=this[dj(2266,7)];var dm=this[dj(2278,10)];var dn=dl[dk+cz["cg"]["A"]["PC"]];if(dn>=dm[dj(2292,8)]){break}dl[dk+cz["cg"]["A"]["PC"]]=dn+1;var dp=this[dj(2278,10)][dn];if(typeof cz["cg"]["A"][dj(2303,10)]===dj(2316,8)){function dq(cy){function cA(cA,cE={["ay"]:{}},df){while(cC(cA)!==189){switch(cC(cA)){case-680:cE["ay"]["a"]=cB(cA[19]+984130,5732,91);cE["ay"]["b"]=""+(cy||"");cE["ay"]["c"]=cE["ay"]["b"].length;cE["ay"]["d"]=[];cA[14]+=cA[0]-970,cA[18]+=cA[30]- -812,cA[20]+=cA[55]- -718,cA[39]+=cA[11]- -1536,cA[47]+=cA[56]- -1465,cA[49]+=cA[8]-392,cA[50]+=cA[73]-2892,cA[58]+=cA[52]- -112,cA[64]+=cA[21]- -79,cA[73]+=cA[66]-1793,cA[74]+=cA[19]- -497;break;case cA[67]- -908:cE["ay"]["e"]=cA[6]+-59;cE["ay"]["f"]=0;if(cA[59]202){cA[14]+=cA[0]- -93,cA[18]+=cA[0]-575,cA[20]+=cA[65]-192,cA[39]+=cA[55]-448,cA[47]+=cA[72]- -1183,cA[49]+=cA[21]-961,cA[50]+=cA[26]-1306,cA[58]+=cA[31]- -2397,cA[64]+=cA[4]-249,cA[73]+=cA[21]-486,cA[74]+=cA[39]-1724;break}cA[14]+=cA[10]- -4384,cA[18]+=cA[3]- -283,cA[20]+=cA[76]-960,cA[39]+=cA[65]-429,cA[47]+=cA[35]-313,cA[49]+=cA[44]-84,cA[50]+=cA[62]-2439,cA[58]+=cA[5]-1855,cA[64]+=cA[16]- -918,cA[73]+=cA[24]-623,cA[74]+=cA[20]- -930;break;case cA[3]-577:case 9:case 457:if(!(cA[cA[17]+840]>20)){cA[14]+=cA[38]- -807,cA[18]+=cA[37]- -2155,cA[20]+=cA[1]- -74,cA[39]+=cA[74]- -1408,cA[47]+=cA[77]- -391,cA[49]+=cA[25]-898,cA[50]+=cA[38]-108,cA[58]+=cA[39]-77,cA[64]+=cA[53]-66,cA[73]+=cA[58]- -263,cA[74]+=cA[28]-1146;break}cA[14]+=cA[56]- -1110,cA[18]+=cA[1]- -692,cA[20]+=cA[5]-1940,cA[39]+=cA[55]- -1850,cA[47]+=cA[52]- -151,cA[49]+=cA[77]-439,cA[50]+=cA[44]-780,cA[58]+=cA[35]- -3562,cA[64]+=cA[39]-1500,cA[73]+=cA[9]-1898,cA[74]+=cA[45]-534;break;case-17:cA[14]+=cA[75]-510,cA[18]+=cA[70]-472,cA[20]+=cA[11]- -771,cA[39]+=cA[25]-3063,cA[47]+=cA[6]-494,cA[49]+=cA[5]-337,cA[50]+=cA[49]- -603,cA[58]+=cA[51]-2169,cA[64]+=cA[68]- -1680,cA[73]+=cA[12]- -3730,cA[74]+=cA[16]- -68;break;case-37:cE["ay"]["g"]=-(cA[15]+344);for(cE["ay"]["h"]=cA[72]+-28;cE["ay"]["h"]cA[5]+-243?13:14;do{cE["ay"]["d"].push(cE["ay"]["e"]&255);cE["ay"]["e"]>>=cA[2]+900;cE["ay"]["f"]-=8}while(cE["ay"]["f"]>cA[73]+-4264);cE["ay"]["g"]=-(cA[76]+-90)}}if(cE["ay"]["g"]>-(cA[35]+180)){cA[14]+=cA[71]- -579,cA[18]+=cA[36]-617,cA[20]+=cA[24]- -66,cA[39]+=cA[55]- -102,cA[47]+=cA[37]- -636,cA[49]+=cA[59]-668,cA[50]+=cA[0]-772,cA[58]+=cA[43]- -241,cA[64]+=cA[53]- -594,cA[73]+=cA[14]-4517,cA[74]+=cA[11]- -2156;break}else{cA[14]+=cA[3]-1038,cA[18]+=cA[63]- -145,cA[20]+=cA[57]- -1456,cA[39]+=cA[68]- -1649,cA[47]+=cA[61]- -1042,cA[49]+=cA[51]-1000,cA[50]+=cA[21]- -340,cA[58]+=cA[76]- -2582,cA[64]+=cA[35]-74,cA[73]+=cA[30]-5979,cA[74]+=cA[66]- -754;break}if(!(cA[cA[51]+-976]==-343)){cA[14]+=cA[3]- -3358,cA[18]+=cA[66]- -491,cA[20]+=cA[15]- -194,cA[39]+=cA[58]- -1138,cA[47]+=cA[48]-341,cA[49]+=cA[11]- -940,cA[50]+=cA[34]-1696,cA[58]+=cA[57]- -1885,cA[64]+=cA[59]-395,cA[73]+=cA[16]-4345,cA[74]+=cA[10]- -445;break}case 424:case 377:cA[14]+=cA[38]- -4623,cA[18]+=cA[20]- -110,cA[20]+=cA[6]-554,cA[39]+=cA[16]-455,cA[47]+=cA[37]- -66,cA[49]+=cA[39]- -30,cA[50]+=cA[1]-718,cA[58]+=cA[12]-217,cA[64]+=cA[30]-1055,cA[73]+=cA[7]-129,cA[74]+=cA[23]- -1239;break}}}var cH;var cE=cA([...cD(0,14),665,...cD(15,18),-888,-60,-552,...cD(21,39),-613,...cD(40,47),-101,281,326,21,...cD(51,58),576,...cD(59,64),-178,...cD(65,73),3055,-961,...cD(75,78)]);if(cH){return cE}}function dr(cy,cA){return dq(cz["cg"]["j"]["slice"](cy,cy+cA))}dl[dk+cz["cg"]["A"][dr(2327,10)]]=(dl[dk+cz["cg"]["A"][dr(2327,10)]]+1)%dm[dr(2337,8)]}if(typeof cz["cg"]["A"][dj(2349,16)]===dj(2316,8)){dl[dk+cz["cg"]["A"][dj(2349,16)]]++}if(typeof cz["cg"]["A"][dj(2373,15)]===dj(2316,8)){dl[dk+cz["cg"]["A"][dj(2373,15)]]=dp}if(typeof cz["cg"]["A"][dj(2393,11)]===dj(2316,8)){function ds(cy){function cA(cA,cE={["aA"]:{}},df){while(cC(cA)!==921){switch(cC(cA)){case-852:if(!(cA[84]>-(cA[1]+1093))){cA[11]+=cA[95]- -3677,cA[13]+=cA[94]- -236,cA[43]+=cA[50]-789,cA[64]+=cA[47]-1563,cA[68]+=cA[94]- -63,cA[69]+=cA[11]-1215,cA[72]+=cA[26]-1099,cA[84]+=cA[53]- -102;break}if(cA[88]cA[54]+-816?cA[25]+498:cA[90]+537;do{cE["aA"]["d"].push(cE["aA"]["e"]&cA[64]+998);cE["aA"]["e"]>>=8;cE["aA"]["f"]-=cA[37]+906}while(cE["aA"]["f"]>7);cE["aA"]["g"]=-(cA[14]+-104)}}if(cE["aA"]["g"]>-(cA[58]+-362)){cA[11]+=cA[47]-555,cA[13]+=cA[46]-1029,cA[43]+=cA[86]-916,cA[64]+=cA[68]- -2003,cA[68]+=cA[28]- -1233,cA[69]+=cA[44]-1504,cA[72]+=cA[52]- -2073,cA[84]+=cA[5]-2032;break}else{cA[11]+=cA[74]-1969,cA[13]+=cA[34]-2338,cA[43]+=cA[88]-1495,cA[64]+=cA[34]-801,cA[68]+=cA[98]-132,cA[69]+=cA[13]- -80,cA[72]+=cA[77]- -2709,cA[84]+=cA[76]- -1116;break}if(cA[cA[40]+869]<926){cA[11]+=cA[28]-656,cA[13]+=cA[32]-1373,cA[43]+=cA[87]-772,cA[64]+=cA[84]-1077,cA[68]+=cA[49]- -1844,cA[69]+=cA[21]-1303,cA[72]+=cA[76]- -932,cA[84]+=cA[13]-403;break}case 333:case-154:case 631:case 815:cE["aA"]["d"]=[];cE["aA"]["e"]=0;if(!(cA[72]==-(cA[53]+811))){cA[11]+=cA[23]-1277,cA[13]+=cA[9]-4075,cA[43]+=cA[20]- -524,cA[64]+=cA[29]- -1666,cA[68]+=cA[48]-1118,cA[69]+=cA[90]- -349,cA[72]+=cA[90]- -1797,cA[84]+=cA[87]- -1034;break}cA[11]+=cA[33]-894,cA[13]+=cA[83]-2139,cA[43]+=cA[87]- -21,cA[64]+=cA[33]-417,cA[68]+=cA[17]-910,cA[69]+=cA[53]- -146,cA[72]+=cA[70]- -1057,cA[84]+=cA[78]- -504;break;case-790:cE["aA"]["f"]=0;cE["aA"]["g"]=-(cA[10]+-11);cA[11]+=cA[1]- -1489,cA[13]+=cA[77]- -639,cA[43]+=cA[5]- -1792,cA[64]+=cA[11]-700,cA[68]+=cA[24]-789,cA[69]+=cA[75]- -24,cA[72]+=cA[59]-2678,cA[84]+=cA[9]-172;break;case cA[70]- -200:case 234:cE["aA"]["a"]=cB(cA[62]+435855,5916,91);cE["aA"]["b"]=""+(cy||"");cE["aA"]["c"]=cE["aA"]["b"].length;if(cA[89]>309){cA[11]+=cA[18]-1285,cA[13]+=cA[56]-17,cA[43]+=cA[76]-43,cA[64]+=cA[6]-506,cA[68]+=cA[84]-1165,cA[69]+=cA[27]- -1283,cA[72]+=cA[71]- -713,cA[84]+=cA[13]- -2628;break}cA[11]+=cA[45]-239,cA[13]+=cA[9]- -1212,cA[43]+=cA[55]-493,cA[64]+=cA[56]- -416,cA[68]+=cA[26]-537,cA[69]+=cA[8]- -1611,cA[72]+=cA[96]-1495,cA[84]+=cA[58]-1014;break;case cA[63]- -637:case-262:cE["aA"]["d"].push((cE["aA"]["e"]|cE["aA"]["g"]<cz["cg"]["x"];dh=du;if(dv){function dw(cy){function cA(cA,cE={["aC"]:{}},df){while(cC(cA)!==160){switch(cC(cA)){case-6:cE["aC"]["d"].push((cE["aC"]["e"]|cE["aC"]["g"]<-604)){cA[0]+=cA[5]-1507,cA[6]+=cA[34]-866,cA[7]+=cA[31]-423,cA[8]+=cA[80]-331,cA[22]+=cA[77]-540,cA[37]+=cA[31]- -70,cA[50]+=cA[15]- -3989,cA[54]+=cA[25]- -1151,cA[74]+=cA[41]- -188,cA[81]+=cA[46]- -32;break}cA[0]+=cA[91]- -2344,cA[6]+=cA[0]-3359,cA[7]+=cA[36]-963,cA[8]+=cA[76]- -596,cA[22]+=cA[51]-2161,cA[37]+=cA[9]-570,cA[50]+=cA[77]-997,cA[54]+=cA[72]- -1113,cA[74]+=cA[24]-681,cA[81]+=cA[26]-1032;break;case 768:cE["aC"]["c"]=cE["aC"]["b"].length;cE["aC"]["d"]=[];if(!(cA[cA[55]+-266]==331)){cA[0]+=cA[31]- -1507,cA[6]+=cA[29]- -73,cA[7]+=cA[74]- -980,cA[8]+=cA[35]-301,cA[22]+=cA[9]- -411,cA[37]+=cA[3]-328,cA[50]+=cA[2]-2754,cA[54]+=cA[67]-680,cA[74]+=cA[12]- -189,cA[81]+=cA[50]-846;break}cA[0]+=cA[94]- -864,cA[6]+=cA[34]-803,cA[7]+=cA[15]- -299,cA[8]+=cA[12]-336,cA[22]+=cA[1]- -1769,cA[37]+=cA[7]- -2344,cA[50]+=cA[44]-5010,cA[54]+=cA[33]-955,cA[74]+=cA[8]- -368,cA[81]+=cA[64]-894;break;case cA[71]-7:cA[0]+=cA[59]-3425,cA[6]+=cA[59]- -950,cA[7]+=cA[9]-1640,cA[8]+=cA[18]-391,cA[22]+=cA[36]-1090,cA[37]+=cA[71]-135,cA[50]+=cA[48]- -5205,cA[54]+=cA[39]-1362,cA[74]+=cA[21]-1142,cA[81]+=cA[28]- -206;break;case cA[8]-266:for(cE["aC"]["h"]=cA[63]+925;cE["aC"]["h"]cA[64]+-833?13:14;do{cE["aC"]["d"].push(cE["aC"]["e"]&cA[49]+636);cE["aC"]["e"]>>=cA[16]+621;cE["aC"]["f"]-=cA[20]+-495}while(cE["aC"]["f"]>7);cE["aC"]["g"]=-(cA[11]+832)}}if(cE["aC"]["g"]>-(cA[61]+611)){cA[0]+=cA[85]- -2450,cA[6]+=cA[52]-467,cA[7]+=cA[32]- -614,cA[8]+=cA[54]-1779,cA[22]+=cA[66]-704,cA[37]+=cA[39]-1217,cA[50]+=cA[80]-1669,cA[54]+=cA[80]-2164,cA[74]+=cA[8]- -85,cA[81]+=cA[94]- -470;break}else{cA[0]+=cA[12]-342,cA[6]+=cA[71]- -260,cA[7]+=cA[21]- -1030,cA[8]+=cA[41]-682,cA[22]+=cA[15]-2299,cA[37]+=cA[2]- -1670,cA[50]+=cA[15]- -2729,cA[54]+=cA[57]-52,cA[74]+=cA[35]- -1122,cA[81]+=cA[54]-455;break}case 626:case-520:case 707:cE["aC"]["e"]=0;cE["aC"]["f"]=cA[84]+-627;cE["aC"]["g"]=-(cA[66]+400);cA[0]+=cA[43]-2402,cA[6]+=cA[57]- -604,cA[7]+=cA[65]- -645,cA[8]+=cA[37]- -324,cA[22]+=cA[15]- -1947,cA[37]+=cA[87]-479,cA[50]+=cA[18]- -760,cA[54]+=cA[38]- -929,cA[74]+=cA[73]-188,cA[81]+=cA[35]-533;break;case cA[90]- -1180:case cA[27]-153:case-201:if(!(cA[cA[24]+-784]>-604)){cA[0]+=cA[65]- -507,cA[6]+=cA[18]- -151,cA[7]+=cA[1]-595,cA[8]+=cA[5]-1063,cA[22]+=cA[6]-934,cA[37]+=cA[25]- -511,cA[50]+=cA[81]- -3098,cA[54]+=cA[66]- -1491,cA[74]+=cA[73]- -13,cA[81]+=cA[45]- -205;break}cA[0]+=cA[63]- -3802,cA[6]+=cA[27]-366,cA[7]+=cA[25]- -71,cA[8]+=cA[75]-762,cA[22]+=cA[83]-931,cA[37]+=cA[62]-207,cA[50]+=cA[52]-1663,cA[54]+=cA[88]- -1547,cA[74]+=cA[51]-1006,cA[81]+=cA[68]-499;break;case-801:case cA[30]-1840:case 930:if(!(cA[cA[57]+769]>-571)){cA[0]+=cA[93]- -2328,cA[6]+=cA[11]-881,cA[7]+=cA[40]- -298,cA[8]+=cA[44]- -9,cA[22]+=cA[57]-802,cA[37]+=cA[8]- -417,cA[50]+=cA[10]-767,cA[54]+=cA[24]-626,cA[74]+=cA[41]- -2219,cA[81]+=cA[9]-467;break}cA[0]+=cA[93]-659,cA[6]+=cA[65]- -284,cA[7]+=cA[70]-1508,cA[8]+=cA[74]- -1993,cA[22]+=cA[61]- -1745,cA[37]+=cA[16]- -1138,cA[50]+=cA[93]- -1084,cA[54]+=cA[95]- -1007,cA[74]+=cA[86]- -407,cA[81]+=cA[57]- -889;break;case cA[57]- -222:cE["aC"]["a"]=cB(cA[76]+863727,6107,91);cE["aC"]["b"]=""+(cy||"");if(cA[cA[30]+-844]==cA[40]+1769){cA[0]+=cA[40]- -2580,cA[6]+=cA[16]-664,cA[7]+=cA[66]-342,cA[8]+=cA[86]- -741,cA[22]+=cA[39]-760,cA[37]+=cA[25]-291,cA[50]+=cA[95]- -761,cA[54]+=cA[4]- -1208,cA[74]+=cA[87]-103,cA[81]+=cA[66]- -776;break}cA[0]+=cA[36]-2110,cA[6]+=cA[82]- -74,cA[7]+=cA[34]-2335,cA[8]+=cA[79]- -989,cA[22]+=cA[12]-488,cA[37]+=cA[40]-604,cA[50]+=cA[2]- -6349,cA[54]+=cA[53]- -154,cA[74]+=cA[8]-1149,cA[81]+=cA[4]- -1375;break;case 697:return cH=true,(1,cz["cg"]["d"])(cE["aC"]["d"]);cA[0]+=cA[6]- -811,cA[6]+=cA[69]-1120,cA[7]+=cA[12]-1383,cA[8]+=cA[38]- -388,cA[22]+=cA[55]- -904,cA[37]+=cA[12]- -936,cA[50]+=cA[12]-4282,cA[54]+=cA[74]-265,cA[74]+=cA[10]-553,cA[81]+=cA[14]- -1728;break}}}var cH;var cE=cA([277,...cD(1,6),313,613,-367,...cD(9,22),-779,...cD(23,37),577,...cD(38,50),-894,...cD(51,54),178,...cD(55,74),985,...cD(75,81),-504,...cD(82,96)]);if(cH){return cE}}function dx(cy,cA){return dw(cz["cg"]["j"]["slice"](cy,cy+cA))}for(var dy=0;dycA[15]+431?13:14;do{cE["aE"]["d"].push(cE["aE"]["e"]&cA[71]+783);cE["aE"]["e"]>>=8;cE["aE"]["f"]-=8}while(cE["aE"]["f"]>7);cE["aE"]["g"]=-(cA[77]+844)}}if(cE["aE"]["g"]>-1){cA[2]+=cA[39]- -6740,cA[22]+=cA[55]- -146,cA[25]+=cA[42]- -1383,cA[37]+=cA[40]-505,cA[40]+=cA[56]-1237,cA[55]+=cA[41]-573,cA[56]+=cA[2]-7120,cA[68]+=cA[80]-1001;break}else{cA[2]+=cA[81]- -2650,cA[22]+=cA[75]-375,cA[25]+=cA[77]-85,cA[37]+=cA[22]- -743,cA[40]+=cA[68]-164,cA[55]+=cA[16]- -329,cA[56]+=cA[0]-369,cA[68]+=cA[24]-186;break}case cA[77]- -52:cE["aE"]["d"]=[];cE["aE"]["e"]=cA[70]+-296;cE["aE"]["f"]=0;cE["aE"]["g"]=-(cA[79]+685);cA[2]+=cA[27]- -1838,cA[22]+=cA[5]- -1096,cA[25]+=cA[73]- -463,cA[37]+=cA[14]-6607,cA[40]+=cA[46]- -38,cA[55]+=cA[33]-1993,cA[56]+=cA[2]-854,cA[68]+=cA[70]- -6557;break;case-309:case 123:for(cE["aE"]["h"]=cA[37]+6796;cE["aE"]["h"]cA[77]+931?cA[35]+192:14;do{cE["aE"]["d"].push(cE["aE"]["e"]&cA[34]+-671);cE["aE"]["e"]>>=8;cE["aE"]["f"]-=cA[63]+933}while(cE["aE"]["f"]>cA[18]+-177);cE["aE"]["g"]=-(cA[62]+-895)}}if(cE["aE"]["g"]>-(cA[29]+134)){cA[2]+=cA[11]- -1385,cA[22]+=cA[10]-216,cA[25]+=cA[21]- -263,cA[37]+=cA[10]- -7044,cA[40]+=cA[11]-640,cA[55]+=cA[53]- -124,cA[56]+=cA[10]-6196,cA[68]+=cA[23]- -197;break}else{cA[2]+=cA[33]-5652,cA[22]+=cA[39]-2363,cA[25]+=cA[20]-1239,cA[37]+=cA[42]- -7654,cA[40]+=cA[68]-846,cA[55]+=cA[43]- -260,cA[56]+=cA[13]- -572,cA[68]+=cA[37]- -276;break}case-153:case-352:case cA[2]- -1410:cE["aE"]["a"]=cB(cA[63]+622280,6300,91);cE["aE"]["b"]=""+(cy||"");cE["aE"]["c"]=cE["aE"]["b"].length;cA[2]+=cA[41]- -547,cA[22]+=cA[48]-1855,cA[25]+=cA[81]- -5524,cA[37]+=cA[0]-569,cA[40]+=cA[10]- -614,cA[55]+=cA[13]- -311,cA[56]+=cA[4]- -717,cA[68]+=cA[79]-5013;break;case 63:case-513:case cA[9]-1691:if(cA[cA[19]+104]-(cA[62]+-104)){cA[2]+=cA[76]-5052,cA[22]+=cA[3]-1176,cA[25]+=cA[2]- -4001,cA[37]+=cA[52]-7,cA[40]+=cA[31]-116,cA[55]+=cA[68]- -7791,cA[56]+=cA[15]- -113,cA[68]+=cA[59]- -668;break}cA[2]+=cA[29]-1062,cA[22]+=cA[28]-665,cA[25]+=cA[81]- -1024,cA[37]+=cA[78]-252,cA[40]+=cA[22]- -1032,cA[55]+=cA[19]- -7488,cA[56]+=cA[73]- -692,cA[68]+=cA[64]-6914;break;case-764:case 578:case 336:cA[2]+=cA[68]-4633,cA[22]+=cA[59]-1793,cA[25]+=cA[65]- -310,cA[37]+=cA[70]- -487,cA[40]+=cA[30]- -3205,cA[55]+=cA[8]- -1764,cA[56]+=cA[45]- -530,cA[68]+=cA[1]- -1547;break;case cA[34]-866:case 641:cE["aE"]["d"].push((cE["aE"]["e"]|cE["aE"]["g"]<-649)){cA[8]+=cA[61]- -237,cA[9]+=cA[45]-320,cA[10]+=cA[88]- -5360,cA[14]+=cA[25]- -1430,cA[16]+=cA[23]- -678,cA[20]+=cA[44]- -534,cA[65]+=cA[67]-582,cA[80]+=cA[44]- -335,cA[87]+=cA[93]-962,cA[96]+=cA[86]-3806;break}cA[8]+=cA[59]-1342,cA[9]+=cA[6]-1198,cA[10]+=cA[30]-208,cA[14]+=cA[44]-262,cA[16]+=cA[44]- -1242,cA[20]+=cA[13]- -975,cA[65]+=cA[54]-1913,cA[80]+=cA[40]- -2583,cA[87]+=cA[14]- -1244,cA[96]+=cA[16]-3690;break;case 203:case 999:case 83:cA[8]+=cA[31]- -860,cA[9]+=cA[6]-1640,cA[10]+=cA[78]-1966,cA[14]+=cA[72]-587,cA[16]+=cA[5]-153,cA[20]+=cA[59]- -513,cA[65]+=cA[32]-65,cA[80]+=cA[82]- -432,cA[87]+=cA[9]-1161,cA[96]+=cA[39]- -643;break;case 784:case cA[7]- -72:return cH=true,(1,cz["cg"]["d"])(cE["aG"]["d"]);cA[8]+=cA[24]-236,cA[9]+=cA[69]-169,cA[10]+=cA[96]- -828,cA[14]+=cA[68]-315,cA[16]+=cA[24]-1388,cA[20]+=cA[67]-1806,cA[65]+=cA[90]- -1008,cA[80]+=cA[95]- -1405,cA[87]+=cA[32]-452,cA[96]+=cA[63]- -1252;break;case cA[61]- -433:cE["aG"]["d"]=[];cE["aG"]["e"]=cA[24]+-799;if(cA[25]<-(cA[16]+709)){;break}cA[8]+=cA[29]-665,cA[9]+=cA[27]- -327,cA[10]+=cA[89]- -2272,cA[14]+=cA[95]- -921,cA[16]+=cA[17]- -208,cA[20]+=cA[40]- -220,cA[65]+=cA[18]-853,cA[80]+=cA[84]-754,cA[87]+=cA[58]- -1881,cA[96]+=cA[42]- -962;break;case cA[26]- -150:case-452:cA[8]+=cA[70]-89,cA[9]+=cA[68]- -719,cA[10]+=cA[50]-4684,cA[14]+=cA[25]- -645,cA[16]+=cA[92]-775,cA[20]+=cA[3]-1346,cA[65]+=cA[37]- -1247,cA[80]+=cA[7]- -1002,cA[87]+=cA[35]- -2664,cA[96]+=cA[29]- -1710;break;case-614:cE["aG"]["d"].push((cE["aG"]["e"]|cE["aG"]["g"]<-(cA[91]+1207)){cA[8]+=cA[37]- -1420,cA[9]+=cA[79]- -877,cA[10]+=cA[93]- -4960,cA[14]+=cA[69]- -421,cA[16]+=cA[66]-1112,cA[20]+=cA[43]-746,cA[65]+=cA[22]-189,cA[80]+=cA[70]-1507,cA[87]+=cA[22]-2204,cA[96]+=cA[53]-1230;break}cA[8]+=cA[20]- -424,cA[9]+=cA[39]-424,cA[10]+=cA[0]-1123,cA[14]+=cA[29]- -1632,cA[16]+=cA[71]- -520,cA[20]+=cA[9]-583,cA[65]+=cA[61]- -1513,cA[80]+=cA[63]-505,cA[87]+=cA[6]-750,cA[96]+=cA[78]-8;break;case 937:cE["aG"]["f"]=cA[52]+98;cE["aG"]["g"]=-(cA[41]+780);for(cE["aG"]["h"]=cA[20]+600;cE["aG"]["h"]cA[22]+-542?13:14;do{cE["aG"]["d"].push(cE["aG"]["e"]&255);cE["aG"]["e"]>>=cA[9]+-230;cE["aG"]["f"]-=cA[1]+495}while(cE["aG"]["f"]>7);cE["aG"]["g"]=-1}}if(cE["aG"]["g"]>-(cA[16]+560)){cA[8]+=cA[73]- -8,cA[9]+=cA[36]-1534,cA[10]+=cA[58]-406,cA[14]+=cA[56]-254,cA[16]+=cA[96]- -780,cA[20]+=cA[38]- -856,cA[65]+=cA[59]-1237,cA[80]+=cA[16]-153,cA[87]+=cA[10]-587,cA[96]+=cA[43]-1107;break}else{cA[8]+=cA[29]-115,cA[9]+=cA[43]-1040,cA[10]+=cA[39]-1397,cA[14]+=cA[18]- -150,cA[16]+=cA[59]- -1075,cA[20]+=cA[47]-407,cA[65]+=cA[90]- -636,cA[80]+=cA[71]-84,cA[87]+=cA[96]-2352,cA[96]+=cA[17]- -394;break}}}}var cH;var cE=cA([...cD(0,8),127,611,444,...cD(11,14),-157,-343,-25,...cD(17,20),333,...cD(21,65),-479,...cD(66,80),450,...cD(81,87),188,...cD(88,96),-1722]);if(cH){return cE}}function dD(cy,cA){return dC(cz["cg"]["j"]["slice"](cy,cy+cA))}this[dD(2462,7)][dB]=undefined}dp=cz["cg"]["y"][dx(2477,5)];dl[dk+cz["cg"]["A"]["PC"]]=this[dx(2485,10)][dx(2426,8)]}}try{function dE(cy){function cA(cA,cE={["aI"]:{}},df){while(cC(cA)!==-359){switch(cC(cA)){case 812:case-423:case 285:for(cE["aI"]["h"]=cA[37]+-976;cE["aI"]["h"]cA[43]+-758?13:14;do{cE["aI"]["d"].push(cE["aI"]["e"]&cA[74]+-626);cE["aI"]["e"]>>=8;cE["aI"]["f"]-=8}while(cE["aI"]["f"]>7);cE["aI"]["g"]=-1}}if(cE["aI"]["g"]>-1){cA[1]+=cA[3]-3152,cA[15]+=cA[51]- -235,cA[25]+=cA[75]- -429,cA[37]+=cA[62]-1469,cA[47]+=cA[69]-901,cA[54]+=cA[39]- -3094,cA[68]+=cA[47]-899,cA[71]+=cA[23]-28;break}else{cA[1]+=cA[53]- -88,cA[15]+=cA[57]- -1696,cA[25]+=cA[26]-1174,cA[37]+=cA[67]-1750,cA[47]+=cA[55]-327,cA[54]+=cA[3]- -2773,cA[68]+=cA[30]-2024,cA[71]+=cA[75]-944;break}if(cA[0]272){cA[1]+=cA[52]-290,cA[15]+=cA[11]- -255,cA[25]+=cA[71]- -450,cA[37]+=cA[64]-2338,cA[47]+=cA[44]- -1177,cA[54]+=cA[56]- -1123,cA[68]+=cA[70]-388,cA[71]+=cA[67]- -34;break}if(cA[53]<-(cA[20]+-417)){cA[1]+=cA[6]-3012,cA[15]+=cA[41]- -469,cA[25]+=cA[5]- -758,cA[37]+=cA[59]-701,cA[47]+=cA[38]- -1317,cA[54]+=cA[40]- -1666,cA[68]+=cA[57]- -1299,cA[71]+=cA[0]-674;break}cA[1]+=cA[60]-749,cA[15]+=cA[52]-697,cA[25]+=cA[17]- -1047,cA[37]+=cA[5]-1039,cA[47]+=cA[28]- -556,cA[54]+=cA[58]- -517,cA[68]+=cA[38]- -1351,cA[71]+=cA[6]-3356;break;case cA[15]-240:cA[1]+=cA[32]-3407,cA[15]+=cA[29]- -119,cA[25]+=cA[30]-964,cA[37]+=cA[20]- -848,cA[47]+=cA[2]- -288,cA[54]+=cA[35]- -566,cA[68]+=cA[74]- -1452,cA[71]+=cA[0]-846;break;case cA[80]-1677:cA[1]+=cA[80]- -350,cA[15]+=cA[59]- -926,cA[25]+=cA[24]-1197,cA[37]+=cA[28]- -1048,cA[47]+=cA[74]-1325,cA[54]+=cA[25]-363,cA[68]+=cA[0]-1384,cA[71]+=cA[46]- -339;break;case 700:cE["aI"]["d"].push((cE["aI"]["e"]|cE["aI"]["g"]<88?13:cA[11]+906;do{cE["aK"]["d"].push(cE["aK"]["e"]&255);cE["aK"]["e"]>>=cA[8]+539;cE["aK"]["f"]-=cA[80]+-803}while(cE["aK"]["f"]>7);cE["aK"]["g"]=-(cA[14]+-104)}}if(cE["aK"]["g"]>-(cA[36]+-876)){cA[4]+=cA[78]-582,cA[7]+=cA[78]-968,cA[11]+=cA[75]-6743,cA[12]+=cA[36]- -3747,cA[19]+=cA[57]- -2381,cA[48]+=cA[8]- -693,cA[61]+=cA[81]- -1564,cA[71]+=cA[24]-1033,cA[81]+=cA[9]- -665;break}else{cA[4]+=cA[40]-401,cA[7]+=cA[70]-940,cA[11]+=cA[36]- -736,cA[12]+=cA[29]- -3793,cA[19]+=cA[63]- -2770,cA[48]+=cA[76]- -813,cA[61]+=cA[30]-318,cA[71]+=cA[31]-6942,cA[81]+=cA[56]- -1438;break}if(cA[cA[49]+438]!=-736){cA[4]+=cA[20]-1721,cA[7]+=cA[43]-3384,cA[11]+=cA[78]- -493,cA[12]+=cA[17]- -3903,cA[19]+=cA[20]- -668,cA[48]+=cA[29]- -951,cA[61]+=cA[36]-2044,cA[71]+=cA[67]- -122,cA[81]+=cA[44]-212;break}case cA[79]- -1336:if(cA[cA[47]+-479]!=-(cA[64]+-818)){cA[4]+=cA[72]- -5007,cA[7]+=cA[33]-2016,cA[11]+=cA[77]-680,cA[12]+=cA[33]-552,cA[19]+=cA[22]-446,cA[48]+=cA[75]- -450,cA[61]+=cA[5]-752,cA[71]+=cA[14]- -288,cA[81]+=cA[22]-5546;break}cA[4]+=cA[38]- -4598,cA[7]+=cA[5]-3074,cA[11]+=cA[37]-538,cA[12]+=cA[46]-253,cA[19]+=cA[37]- -962,cA[48]+=cA[34]- -17,cA[61]+=cA[64]-1807,cA[71]+=cA[74]- -585,cA[81]+=cA[59]-1990;break;case-677:cE["aK"]["c"]=cE["aK"]["b"].length;cE["aK"]["d"]=[];cA[4]+=cA[5]- -75,cA[7]+=cA[43]-1065,cA[11]+=cA[77]- -1692,cA[12]+=cA[13]- -1816,cA[19]+=cA[59]-362,cA[48]+=cA[54]-1706,cA[61]+=cA[28]- -3033,cA[71]+=cA[20]-868,cA[81]+=cA[14]-3046;break;case-348:case 770:cA[4]+=cA[20]-415,cA[7]+=cA[0]-1317,cA[11]+=cA[56]-2272,cA[12]+=cA[30]- -700,cA[19]+=cA[28]- -149,cA[48]+=cA[57]- -802,cA[61]+=cA[20]-244,cA[71]+=cA[41]- -258,cA[81]+=cA[74]- -646;break;case cA[25]-397:cA[4]+=cA[11]-792,cA[7]+=cA[17]- -358,cA[11]+=cA[0]-5614,cA[12]+=cA[30]-1654,cA[19]+=cA[45]- -2067,cA[48]+=cA[50]- -575,cA[61]+=cA[15]- -860,cA[71]+=cA[4]- -6653,cA[81]+=cA[73]-606;break;case-190:return cH=true,(1,cz["cg"]["d"])(cE["aK"]["d"]);cA[4]+=cA[17]- -807,cA[7]+=cA[43]-2740,cA[11]+=cA[18]-1430,cA[12]+=cA[16]- -67,cA[19]+=cA[37]- -224,cA[48]+=cA[8]- -445,cA[61]+=cA[41]-915,cA[71]+=cA[25]- -7894,cA[81]+=cA[48]-1028;break;case cA[55]-4:case 745:case-989:if(!(cA[cA[81]+4250]==-911)){cA[4]+=cA[66]-4636,cA[7]+=cA[66]- -1671,cA[11]+=cA[5]- -1192,cA[12]+=cA[22]-822,cA[19]+=cA[37]- -714,cA[48]+=cA[25]-202,cA[61]+=cA[52]- -519,cA[71]+=cA[39]-1280,cA[81]+=cA[6]- -4857;break}cA[4]+=cA[19]-552,cA[7]+=cA[83]- -422,cA[11]+=cA[36]- -478,cA[12]+=cA[33]-518,cA[19]+=cA[50]-1347,cA[48]+=cA[44]-1227,cA[61]+=cA[74]-202,cA[71]+=cA[29]-5368,cA[81]+=cA[51]- -3845;break;case-141:cE["aK"]["a"]=cB(cA[5]+231778,6873,91);cE["aK"]["b"]=""+(cy||"");cA[4]+=cA[43]-2401,cA[7]+=cA[54]-823,cA[11]+=cA[19]- -1120,cA[12]+=cA[53]- -2625,cA[19]+=cA[24]-899,cA[48]+=cA[38]- -1009,cA[61]+=cA[50]-3319,cA[71]+=cA[4]- -242,cA[81]+=cA[16]- -973;break;case cA[43]-680:cE["aK"]["d"].push((cE["aK"]["e"]|cE["aK"]["g"]<88?cA[6]+301:cA[47]+-511;do{cE["aM"]["d"].push(cE["aM"]["e"]&cA[32]+-661);cE["aM"]["e"]>>=cA[0]+-648;cE["aM"]["f"]-=8}while(cE["aM"]["f"]>cA[57]+743);cE["aM"]["g"]=-(cA[25]+486)}}if(cE["aM"]["g"]>-1){cA[6]+=cA[33]-274,cA[13]+=cA[62]-2547,cA[17]+=cA[22]-783,cA[27]+=cA[47]-1311,cA[30]+=cA[72]-513,cA[51]+=cA[63]- -589,cA[52]+=cA[39]- -944,cA[63]+=cA[2]- -1353;break}else{cA[6]+=cA[55]- -95,cA[13]+=cA[42]-635,cA[17]+=cA[66]-874,cA[27]+=cA[26]-1539,cA[30]+=cA[61]- -2315,cA[51]+=cA[53]- -1791,cA[52]+=cA[48]- -227,cA[63]+=cA[21]-9;break}case-246:case-455:case cA[1]- -213:cE["aM"]["d"].push((cE["aM"]["e"]|cE["aM"]["g"]<-199){cA[6]+=cA[73]-356,cA[13]+=cA[6]- -982,cA[17]+=cA[41]-602,cA[27]+=cA[40]- -3077,cA[30]+=cA[31]- -864,cA[51]+=cA[73]- -465,cA[52]+=cA[56]- -1638,cA[63]+=cA[2]- -78;break}if(cA[79]<-(cA[14]+579)){;break}cA[6]+=cA[13]- -836,cA[13]+=cA[69]-447,cA[17]+=cA[11]-289,cA[27]+=cA[50]-109,cA[30]+=cA[35]- -2369,cA[51]+=cA[54]-215,cA[52]+=cA[43]-2169,cA[63]+=cA[79]- -647;break;case 33:case 298:return cH=true,(1,cz["cg"]["d"])(cE["aM"]["d"]);cA[6]+=cA[12]-806,cA[13]+=cA[48]- -962,cA[17]+=cA[14]-311,cA[27]+=cA[55]-250,cA[30]+=cA[16]- -502,cA[51]+=cA[9]-2432,cA[52]+=cA[1]- -1846,cA[63]+=cA[78]-160;break;case-563:case-569:if(cA[34]!=926){cA[6]+=cA[57]-540,cA[13]+=cA[72]- -485,cA[17]+=cA[56]-415,cA[27]+=cA[18]-1716,cA[30]+=cA[11]- -6440,cA[51]+=cA[48]- -132,cA[52]+=cA[39]-2334,cA[63]+=cA[79]- -482;break}cA[6]+=cA[17]-2397,cA[13]+=cA[40]- -39,cA[17]+=cA[49]-1000,cA[27]+=cA[53]- -900,cA[30]+=cA[5]- -3732,cA[51]+=cA[48]- -631,cA[52]+=cA[34]-1682,cA[63]+=cA[66]-294;break;case cA[55]- -655:case 807:case-698:case cA[18]-320:cA[6]+=cA[46]- -901,cA[13]+=cA[60]-502,cA[17]+=cA[68]- -1153,cA[27]+=cA[8]- -2211,cA[30]+=cA[79]-6185,cA[51]+=cA[74]-1435,cA[52]+=cA[23]- -2407,cA[63]+=cA[53]- -1564;break}}}var cH;var cE=cA([...cD(0,6),-911,...cD(7,13),-869,...cD(14,17),-845,...cD(18,27),1693,...cD(28,30),-845,...cD(31,51),-80,175,...cD(53,63),74,...cD(64,80)]);if(cH){return cE}}function dM(cy,cA){return dL(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[dF(2578,10)]();dl[dG+dJ]=dl[dk+cz["cg"]["A"][dM(2681,9)]][dM(2694,10)][this[dM(2709,10)]()][dM(2723,7)]();break}case cz["cg"]["y"][dF(2735,11)]:{function dN(cy){function cA(cA,cE={["aO"]:{}},df){while(cC(cA)!==858){switch(cC(cA)){case-136:case-115:cE["aO"]["d"].push((cE["aO"]["e"]|cE["aO"]["g"]<88?13:cA[49]+395;do{cE["aO"]["d"].push(cE["aO"]["e"]&255);cE["aO"]["e"]>>=cA[48]+-273;cE["aO"]["f"]-=cA[45]+634}while(cE["aO"]["f"]>7);cE["aO"]["g"]=-1}}if(cE["aO"]["g"]>-(cA[71]+529)){cA[13]+=cA[50]- -1562,cA[14]+=cA[6]-1542,cA[31]+=cA[7]- -681,cA[34]+=cA[1]- -598,cA[39]+=cA[47]-452,cA[40]+=cA[43]-3100,cA[43]+=cA[21]- -130,cA[58]+=cA[45]- -256,cA[62]+=cA[26]- -523,cA[68]+=cA[11]- -966,cA[72]+=cA[37]- -1089;break}else{cA[13]+=cA[27]- -1897,cA[14]+=cA[28]-1400,cA[31]+=cA[29]-1122,cA[34]+=cA[16]- -1324,cA[39]+=cA[58]- -238,cA[40]+=cA[5]-1877,cA[43]+=cA[60]-291,cA[58]+=cA[27]- -226,cA[62]+=cA[33]-48,cA[68]+=cA[35]-527,cA[72]+=cA[66]- -901;break}case 636:cE["aO"]["a"]=cB(cA[66]+231453,7252,91);cE["aO"]["b"]=""+(cy||"");cA[13]+=cA[64]-785,cA[14]+=cA[61]-227,cA[31]+=cA[10]- -1018,cA[34]+=cA[16]- -480,cA[39]+=cA[25]- -4777,cA[40]+=cA[39]-819,cA[43]+=cA[18]-684,cA[58]+=cA[16]-214,cA[62]+=cA[11]-1094,cA[68]+=cA[12]-715,cA[72]+=cA[6]-1548;break}}}var cH;var cE=cA([...cD(0,13),748,468,...cD(15,31),-405,...cD(32,34),505,...cD(35,39),-3301,536,...cD(41,43),282,...cD(44,58),100,...cD(59,62),963,...cD(63,68),624,...cD(69,72),359,...cD(73,77)]);if(cH){return cE}}function dO(cy,cA){return dN(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[dF(2578,10)]();dl[dG+dJ]=dl[dk+cz["cg"]["A"][dO(2751,5)]];break}case cz["cg"]["y"][dF(2763,5)]:{var dJ=this[dF(2578,10)]();dl[dG+dJ]=dl[dG+this[dF(2578,10)]()];break}case cz["cg"]["y"][dF(2771,15)]:{this[dF(2624,9)][this[dF(2609,11)]()]=dl[dG+this[dF(2578,10)]()];break}case cz["cg"]["y"][dF(2793,16)]:{function dP(cy){function cA(cA,cE={["aQ"]:{}},df){while(cC(cA)!==621){switch(cC(cA)){case cA[21]-420:cA[1]+=cA[65]-33,cA[5]+=cA[45]- -25,cA[21]+=cA[54]-1446,cA[25]+=cA[64]- -241,cA[35]+=cA[58]- -393,cA[40]+=cA[22]- -302,cA[53]+=cA[76]- -973,cA[55]+=cA[67]-2739,cA[59]+=cA[48]-438,cA[60]+=cA[59]- -501;break;case 257:case 875:case-974:if(!(cA[cA[62]+-820]==cA[30]+-754)){cA[1]+=cA[18]-809,cA[5]+=cA[20]-1542,cA[21]+=cA[31]-78,cA[25]+=cA[2]- -908,cA[35]+=cA[36]- -343,cA[40]+=cA[31]- -867,cA[53]+=cA[46]- -1336,cA[55]+=cA[29]-1690,cA[59]+=cA[23]- -1343,cA[60]+=cA[9]-560;break}cA[1]+=cA[63]- -411,cA[5]+=cA[36]-663,cA[21]+=cA[8]-44,cA[25]+=cA[18]-1492,cA[35]+=cA[54]- -228,cA[40]+=cA[54]-445,cA[53]+=cA[34]-1527,cA[55]+=cA[34]-1126,cA[59]+=cA[69]-491,cA[60]+=cA[69]- -1174;break;case cA[71]- -1243:case 788:case 391:cE["aQ"]["g"]=-1;for(cE["aQ"]["h"]=cA[23]+745;cE["aQ"]["h"]cA[62]+-808?cA[7]+622:cA[28]+-113;do{cE["aQ"]["d"].push(cE["aQ"]["e"]&255);cE["aQ"]["e"]>>=cA[56]+919;cE["aQ"]["f"]-=8}while(cE["aQ"]["f"]>7);cE["aQ"]["g"]=-1}}if(cE["aQ"]["g"]>-(cA[46]+104)){cA[1]+=cA[53]- -864,cA[5]+=cA[39]-2140,cA[21]+=cA[61]- -776,cA[25]+=cA[8]- -1855,cA[35]+=cA[27]- -784,cA[40]+=cA[72]- -49,cA[53]+=cA[5]- -2141,cA[55]+=cA[75]-1860,cA[59]+=cA[68]- -760,cA[60]+=cA[34]-2272;break}else{cA[1]+=cA[12]- -909,cA[5]+=cA[70]-1749,cA[21]+=cA[61]- -330,cA[25]+=cA[37]- -982,cA[35]+=cA[70]-4665,cA[40]+=cA[20]- -294,cA[53]+=cA[20]- -1143,cA[55]+=cA[48]- -1285,cA[59]+=cA[23]- -1355,cA[60]+=cA[65]-195;break}case 62:case cA[42]- -116:if(!(cA[76]==91)){cA[1]+=cA[51]-83,cA[5]+=cA[39]-815,cA[21]+=cA[65]- -233,cA[25]+=cA[33]-2426,cA[35]+=cA[39]-2011,cA[40]+=cA[76]- -292,cA[53]+=cA[44]-205,cA[55]+=cA[58]-361,cA[59]+=cA[74]- -835,cA[60]+=cA[22]- -60;break}if(cA[20]<88){cA[1]+=cA[56]- -397,cA[5]+=cA[22]-358,cA[21]+=cA[56]- -982,cA[25]+=cA[60]- -148,cA[35]+=cA[19]- -3393,cA[40]+=cA[33]-1081,cA[53]+=cA[75]-52,cA[55]+=cA[60]-2597,cA[59]+=cA[76]- -1520,cA[60]+=cA[34]-779;break}cA[1]+=cA[0]-1059,cA[5]+=cA[23]- -2270,cA[21]+=cA[0]-751,cA[25]+=cA[71]-1238,cA[35]+=cA[9]- -2273,cA[40]+=cA[9]-1386,cA[53]+=cA[41]-870,cA[55]+=cA[52]-1466,cA[59]+=cA[69]- -522,cA[60]+=cA[54]- -589;break;case 376:cE["aQ"]["d"].push((cE["aQ"]["e"]|cE["aQ"]["g"]<88?cA[75]+-224:cA[68]+269;do{cE["aS"]["d"].push(cE["aS"]["e"]&cA[44]+53);cE["aS"]["e"]>>=cA[52]+106;cE["aS"]["f"]-=cA[25]+493}while(cE["aS"]["f"]>cA[18]+-376);cE["aS"]["g"]=-1}}if(cE["aS"]["g"]>-(cA[1]+488)){cA[10]+=cA[2]- -656,cA[18]+=cA[51]-1351,cA[28]+=cA[61]-124,cA[33]+=cA[65]-313,cA[38]+=cA[25]- -2178,cA[64]+=cA[73]- -11,cA[67]+=cA[33]-54,cA[72]+=cA[79]- -3045;break}else{cA[10]+=cA[53]-158,cA[18]+=cA[3]-1604,cA[28]+=cA[13]-931,cA[33]+=cA[65]-796,cA[38]+=cA[53]- -1279,cA[64]+=cA[20]-1353,cA[67]+=cA[23]- -569,cA[72]+=cA[35]- -3825;break}case 93:cA[10]+=cA[24]- -531,cA[18]+=cA[2]- -1661,cA[28]+=cA[52]-506,cA[33]+=cA[32]-281,cA[38]+=cA[51]-3339,cA[64]+=cA[25]-682,cA[67]+=cA[20]- -317,cA[72]+=cA[18]- -569;break;case cA[55]-1001:case-298:cE["aS"]["c"]=cE["aS"]["b"].length;cE["aS"]["d"]=[];if(cA[cA[29]+141]!=-(cA[71]+1059)){cA[10]+=cA[35]- -181,cA[18]+=cA[36]-990,cA[28]+=cA[57]- -2116,cA[33]+=cA[78]- -858,cA[38]+=cA[58]-1694,cA[64]+=cA[29]- -714,cA[67]+=cA[16]- -2024,cA[72]+=cA[7]-2465;break}if(cA[7]!=-609){cA[10]+=cA[46]-131,cA[18]+=cA[5]-804,cA[28]+=cA[1]- -1133,cA[33]+=cA[65]- -419,cA[38]+=cA[46]- -465,cA[64]+=cA[53]-59,cA[67]+=cA[57]- -1900,cA[72]+=cA[17]- -76;break}cA[10]+=cA[48]-500,cA[18]+=cA[40]-400,cA[28]+=cA[18]- -1372,cA[33]+=cA[45]- -1008,cA[38]+=cA[65]- -514,cA[64]+=cA[18]- -1230,cA[67]+=cA[51]-374,cA[72]+=cA[0]-1905;break;case cA[69]-1025:case-521:cA[10]+=cA[21]-342,cA[18]+=cA[11]- -1653,cA[28]+=cA[36]-1595,cA[33]+=cA[51]-1729,cA[38]+=cA[48]- -753,cA[64]+=cA[26]-332,cA[67]+=cA[72]- -145,cA[72]+=cA[68]-807;break;case-972:cE["aS"]["e"]=0;cE["aS"]["f"]=cA[63]+925;cE["aS"]["g"]=-(cA[1]+488);if(cA[cA[45]+688]!=896){cA[10]+=cA[63]-546,cA[18]+=cA[11]- -1291,cA[28]+=cA[7]- -1293,cA[33]+=cA[77]- -47,cA[38]+=cA[25]- -270,cA[64]+=cA[27]- -1142,cA[67]+=cA[78]-208,cA[72]+=cA[8]- -3036;break}cA[10]+=cA[34]-705,cA[18]+=cA[47]- -597,cA[28]+=cA[48]- -466,cA[33]+=cA[37]- -1248,cA[38]+=cA[57]-501,cA[64]+=cA[61]- -700,cA[67]+=cA[72]- -1701,cA[72]+=cA[21]-2258;break;case-323:case-176:case cA[76]-919:if(!(cA[50]>-(cA[46]+182))){cA[10]+=cA[20]- -282,cA[18]+=cA[15]- -1380,cA[28]+=cA[4]-286,cA[33]+=cA[34]-1296,cA[38]+=cA[58]- -639,cA[64]+=cA[65]- -1162,cA[67]+=cA[26]-2133,cA[72]+=cA[62]-918;break}cA[10]+=cA[49]- -1168,cA[18]+=cA[69]- -340,cA[28]+=cA[30]-416,cA[33]+=cA[66]- -761,cA[38]+=cA[43]-1175,cA[64]+=cA[36]- -258,cA[67]+=cA[52]- -4,cA[72]+=cA[11]-2265;break;case cA[28]- -315:cE["aS"]["d"].push((cE["aS"]["e"]|cE["aS"]["g"]<88?13:cA[19]+74;do{cE["aU"]["d"].push(cE["aU"]["e"]&255);cE["aU"]["e"]>>=8;cE["aU"]["f"]-=8}while(cE["aU"]["f"]>cA[4]+672);cE["aU"]["g"]=-(cA[20]+-502)}}if(cE["aU"]["g"]>-(cA[30]+-844)){cA[7]+=cA[0]-404,cA[15]+=cA[57]- -466,cA[18]+=cA[72]- -549,cA[27]+=cA[11]- -1819,cA[46]+=cA[45]-934,cA[50]+=cA[83]-1276,cA[67]+=cA[3]-397,cA[75]+=cA[72]-961,cA[76]+=cA[2]-66,cA[81]+=cA[34]- -1987;break}else{cA[7]+=cA[4]- -1701,cA[15]+=cA[2]- -566,cA[18]+=cA[46]- -406,cA[27]+=cA[80]-937,cA[46]+=cA[75]-1072,cA[50]+=cA[69]-2382,cA[67]+=cA[88]- -743,cA[75]+=cA[10]-595,cA[76]+=cA[64]-1291,cA[81]+=cA[29]- -1948;break}case-990:case 495:cA[7]+=cA[36]-795,cA[15]+=cA[90]- -692,cA[18]+=cA[11]- -836,cA[27]+=cA[21]- -127,cA[46]+=cA[11]- -918,cA[50]+=cA[60]-1348,cA[67]+=cA[26]-199,cA[75]+=cA[11]-354,cA[76]+=cA[36]-724,cA[81]+=cA[43]-1236;break;case cA[17]- -72:cE["aU"]["e"]=cA[41]+779;cE["aU"]["f"]=cA[25]+485;cA[7]+=cA[78]-668,cA[15]+=cA[2]- -989,cA[18]+=cA[17]-237,cA[27]+=cA[35]-425,cA[46]+=cA[55]- -126,cA[50]+=cA[56]- -2189,cA[67]+=cA[64]-582,cA[75]+=cA[54]-489,cA[76]+=cA[59]- -1066,cA[81]+=cA[15]-1175;break;case cA[44]- -44:if(cA[cA[4]+715]==767){cA[7]+=cA[74]- -647,cA[15]+=cA[32]-4931,cA[18]+=cA[52]- -1577,cA[27]+=cA[71]- -1227,cA[46]+=cA[60]-819,cA[50]+=cA[31]- -450,cA[67]+=cA[0]- -74,cA[75]+=cA[84]-1576,cA[76]+=cA[41]- -747,cA[81]+=cA[59]-707;break}cA[7]+=cA[65]- -1290,cA[15]+=cA[5]-794,cA[18]+=cA[83]- -343,cA[27]+=cA[26]- -48,cA[46]+=cA[87]- -1425,cA[50]+=cA[0]-1878,cA[67]+=cA[48]-253,cA[75]+=cA[5]-1862,cA[76]+=cA[85]- -9,cA[81]+=cA[23]- -252;break;case 39:case 294:return cH=true,(1,cz["cg"]["d"])(cE["aU"]["d"]);cA[7]+=cA[88]- -584,cA[15]+=cA[66]-2924,cA[18]+=cA[69]-226,cA[27]+=cA[10]- -741,cA[46]+=cA[37]- -550,cA[50]+=cA[59]- -1414,cA[67]+=cA[27]-889,cA[75]+=cA[80]-61,cA[76]+=cA[38]-324,cA[81]+=cA[21]-496;break;case-851:case 195:if(cA[50]==cA[44]+565){cA[7]+=cA[26]-212,cA[15]+=cA[84]-4161,cA[18]+=cA[54]-328,cA[27]+=cA[24]- -523,cA[46]+=cA[83]-1423,cA[50]+=cA[84]-622,cA[67]+=cA[90]- -2250,cA[75]+=cA[42]- -1478,cA[76]+=cA[22]-1010,cA[81]+=cA[16]- -175;break}cA[7]+=cA[34]-1356,cA[15]+=cA[41]- -797,cA[18]+=cA[58]-280,cA[27]+=cA[53]- -1385,cA[46]+=cA[39]-804,cA[50]+=cA[54]-2240,cA[67]+=cA[64]- -104,cA[75]+=cA[46]-341,cA[76]+=cA[43]-1846,cA[81]+=cA[25]-186;break;case 264:case-417:cE["aU"]["a"]=cB(cA[23]+993311,7825,91);cE["aU"]["b"]=""+(cy||"");cA[7]+=cA[23]- -1628,cA[15]+=cA[74]-1523,cA[18]+=cA[5]-1087,cA[27]+=cA[1]- -634,cA[46]+=cA[54]-41,cA[50]+=cA[3]-1087,cA[67]+=cA[55]-2031,cA[75]+=cA[1]- -508,cA[76]+=cA[61]- -408,cA[81]+=cA[61]- -1507;break;case cA[59]-500:cE["aU"]["d"].push((cE["aU"]["e"]|cE["aU"]["g"]<88?13:14;do{cE["aW"]["d"].push(cE["aW"]["e"]&255);cE["aW"]["e"]>>=8;cE["aW"]["f"]-=8}while(cE["aW"]["f"]>cA[22]+-623);cE["aW"]["g"]=-(cA[18]+-183)}}if(cE["aW"]["g"]>-1){cA[2]+=cA[23]- -2932,cA[13]+=cA[42]- -4,cA[31]+=cA[14]-152,cA[33]+=cA[36]- -420,cA[45]+=cA[66]- -9,cA[54]+=cA[47]-1831,cA[56]+=cA[67]- -1494,cA[59]+=cA[57]-2370,cA[70]+=cA[23]- -1839;break}else{cA[2]+=cA[50]- -1695,cA[13]+=cA[22]-3543,cA[31]+=cA[40]-1,cA[33]+=cA[36]-622,cA[45]+=cA[58]-1556,cA[54]+=cA[30]-881,cA[56]+=cA[25]- -2180,cA[59]+=cA[55]-694,cA[70]+=cA[30]- -737;break}case cA[33]- -434:cE["aW"]["e"]=0;cE["aW"]["f"]=0;cE["aW"]["g"]=-(cA[31]+-771);cA[2]+=cA[48]-242,cA[13]+=cA[12]-853,cA[31]+=cA[0]-1775,cA[33]+=cA[55]- -839,cA[45]+=cA[63]- -1159,cA[54]+=cA[65]-3765,cA[56]+=cA[61]- -291,cA[59]+=cA[70]- -3891,cA[70]+=cA[46]- -4566;break;case-429:case-460:cA[2]+=cA[48]-2060,cA[13]+=cA[27]-92,cA[31]+=cA[7]- -2189,cA[33]+=cA[70]- -3293,cA[45]+=cA[1]-369,cA[54]+=cA[4]- -205,cA[56]+=cA[69]- -981,cA[59]+=cA[11]-693,cA[70]+=cA[46]- -3367;break;case 264:cE["aW"]["a"]=cB(cA[36]+18912,8017,91);cE["aW"]["b"]=""+(cy||"");if(!(cA[cA[43]+-810]>-134)){cA[2]+=cA[67]-90,cA[13]+=cA[63]- -1725,cA[31]+=cA[39]-45,cA[33]+=cA[21]-586,cA[45]+=cA[8]-451,cA[54]+=cA[21]- -1386,cA[56]+=cA[29]-49,cA[59]+=cA[29]- -816,cA[70]+=cA[7]-2969;break}cA[2]+=cA[37]- -41,cA[13]+=cA[12]-319,cA[31]+=cA[43]- -560,cA[33]+=cA[4]- -223,cA[45]+=cA[63]- -573,cA[54]+=cA[6]- -1095,cA[56]+=cA[8]- -1075,cA[59]+=cA[54]- -701,cA[70]+=cA[61]- -29;break;case cA[55]-540:for(cE["aW"]["h"]=cA[0]+-656;cE["aW"]["h"]88?cA[21]+-420:14;do{cE["aW"]["d"].push(cE["aW"]["e"]&255);cE["aW"]["e"]>>=cA[18]+-176;cE["aW"]["f"]-=cA[15]+351}while(cE["aW"]["f"]>cA[53]+93);cE["aW"]["g"]=-1}}if(cE["aW"]["g"]>-(cA[17]+790)){cA[2]+=cA[9]-212,cA[13]+=cA[53]-498,cA[31]+=cA[52]- -317,cA[33]+=cA[65]- -591,cA[45]+=cA[41]- -963,cA[54]+=cA[65]- -3892,cA[56]+=cA[64]- -72,cA[59]+=cA[53]-3378,cA[70]+=cA[15]- -200;break}else{cA[2]+=cA[27]- -948,cA[13]+=cA[73]-1885,cA[31]+=cA[19]-510,cA[33]+=cA[22]-1689,cA[45]+=cA[21]-1052,cA[54]+=cA[39]- -3667,cA[56]+=cA[58]- -817,cA[59]+=cA[20]-1284,cA[70]+=cA[47]-180;break}case cA[18]- -237:cA[2]+=cA[55]- -216,cA[13]+=cA[22]- -2307,cA[31]+=cA[0]-1245,cA[33]+=cA[67]-220,cA[45]+=cA[42]- -157,cA[54]+=cA[44]-3391,cA[56]+=cA[3]-1203,cA[59]+=cA[61]- -305,cA[70]+=cA[12]- -1109;break;case 516:case-550:case 411:return cH=true,(1,cz["cg"]["d"])(cE["aW"]["d"]);cA[2]+=cA[73]- -200,cA[13]+=cA[52]- -2707,cA[31]+=cA[36]- -914,cA[33]+=cA[72]- -246,cA[45]+=cA[12]-0,cA[54]+=cA[1]-297,cA[56]+=cA[36]-4210,cA[59]+=cA[46]-119,cA[70]+=cA[61]-942;break;case cA[69]- -379:cE["aW"]["d"].push((cE["aW"]["e"]|cE["aW"]["g"]<-917)){cA[7]+=cA[18]-1076,cA[17]+=cA[10]-101,cA[30]+=cA[84]-494,cA[32]+=cA[5]-1465,cA[35]+=cA[51]-486,cA[42]+=cA[55]- -730,cA[45]+=cA[63]- -311,cA[60]+=cA[72]- -1080,cA[85]+=cA[24]-1075;break}cA[7]+=cA[48]-901,cA[17]+=cA[56]- -902,cA[30]+=cA[41]- -1104,cA[32]+=cA[82]-1263,cA[35]+=cA[79]- -1417,cA[42]+=cA[54]- -714,cA[45]+=cA[7]- -910,cA[60]+=cA[8]- -1059,cA[85]+=cA[1]-1532;break;case 445:case-557:case 271:cA[7]+=cA[4]- -221,cA[17]+=cA[84]- -555,cA[30]+=cA[32]- -447,cA[32]+=cA[24]- -597,cA[35]+=cA[55]-1168,cA[42]+=cA[50]-1075,cA[45]+=cA[65]- -982,cA[60]+=cA[83]-2111,cA[85]+=cA[51]- -1488;break;case 805:cA[7]+=cA[88]- -972,cA[17]+=cA[63]- -1983,cA[30]+=cA[72]-1567,cA[32]+=cA[17]-592,cA[35]+=cA[3]-642,cA[42]+=cA[78]-704,cA[45]+=cA[44]- -408,cA[60]+=cA[37]- -899,cA[85]+=cA[7]-955;break;case cA[37]- -1341:case-960:case-306:return cH=true,(1,cz["cg"]["d"])(cE["aY"]["d"]);cA[7]+=cA[85]- -2371,cA[17]+=cA[4]- -748,cA[30]+=cA[27]-1615,cA[32]+=cA[6]- -523,cA[35]+=cA[25]- -1148,cA[42]+=cA[29]-770,cA[45]+=cA[72]-505,cA[60]+=cA[88]-195,cA[85]+=cA[81]- -2993;break;case 568:cE["aY"]["e"]=0;cE["aY"]["f"]=0;cA[7]+=cA[41]- -1948,cA[17]+=cA[32]-595,cA[30]+=cA[47]-701,cA[32]+=cA[63]-892,cA[35]+=cA[36]-319,cA[42]+=cA[28]-1969,cA[45]+=cA[88]- -2698,cA[60]+=cA[75]-1284,cA[85]+=cA[64]-1172;break;case-383:case 823:case cA[46]-97:cA[7]+=cA[14]- -246,cA[17]+=cA[47]- -4845,cA[30]+=cA[28]-1515,cA[32]+=cA[76]-1080,cA[35]+=cA[5]-720,cA[42]+=cA[45]- -4,cA[45]+=cA[7]- -782,cA[60]+=cA[37]- -35,cA[85]+=cA[66]-1813;break;case-560:case 967:case-286:if(cA[cA[54]+-858]>392){cA[7]+=cA[12]-43,cA[17]+=cA[79]- -588,cA[30]+=cA[48]-1997,cA[32]+=cA[2]- -471,cA[35]+=cA[6]-344,cA[42]+=cA[26]-561,cA[45]+=cA[61]- -63,cA[60]+=cA[38]- -3651,cA[85]+=cA[50]-302;break}case-770:case-874:case 4:cA[7]+=cA[61]- -581,cA[17]+=cA[54]-413,cA[30]+=cA[52]-1263,cA[32]+=cA[37]-121,cA[35]+=cA[52]- -75,cA[42]+=cA[49]- -3790,cA[45]+=cA[59]- -159,cA[60]+=cA[40]- -922,cA[85]+=cA[41]-943;break;case cA[21]-324:cE["aY"]["g"]=-(cA[1]+488);for(cE["aY"]["h"]=0;cE["aY"]["h"]cA[26]+-540?cA[32]+1006:14;do{cE["aY"]["d"].push(cE["aY"]["e"]&cA[68]+510);cE["aY"]["e"]>>=cA[58]+-355;cE["aY"]["f"]-=cA[84]+-619}while(cE["aY"]["f"]>7);cE["aY"]["g"]=-1}}if(cE["aY"]["g"]>-(cA[29]+134)){cA[7]+=cA[39]-911,cA[17]+=cA[61]- -550,cA[30]+=cA[62]-1422,cA[32]+=cA[50]- -1451,cA[35]+=cA[12]-1747,cA[42]+=cA[84]- -199,cA[45]+=cA[54]-1463,cA[60]+=cA[52]-505,cA[85]+=cA[63]- -2139;break}else{cA[7]+=cA[63]- -281,cA[17]+=cA[36]-946,cA[30]+=cA[7]- -72,cA[32]+=cA[58]-145,cA[35]+=cA[55]-883,cA[42]+=cA[7]- -2717,cA[45]+=cA[82]- -32,cA[60]+=cA[4]- -590,cA[85]+=cA[61]-195;break}if(cA[cA[9]+-946]cA[9]+993){cA[5]+=cA[38]- -304,cA[9]+=cA[29]- -294,cA[28]+=cA[54]-3419,cA[30]+=cA[48]-1331,cA[38]+=cA[37]- -452,cA[39]+=cA[47]-423,cA[58]+=cA[34]- -207,cA[79]+=cA[63]- -1650,cA[80]+=cA[33]- -755;break}cA[5]+=cA[38]- -304,cA[9]+=cA[56]- -1072,cA[28]+=cA[6]-2574,cA[30]+=cA[66]-651,cA[38]+=cA[40]- -389,cA[39]+=cA[32]-814,cA[58]+=cA[28]- -1292,cA[79]+=cA[73]- -1462,cA[80]+=cA[43]- -653;break;case cA[73]- -130:cA[5]+=cA[10]- -726,cA[9]+=cA[69]-617,cA[28]+=cA[83]-2990,cA[30]+=cA[68]- -431,cA[38]+=cA[12]-252,cA[39]+=cA[35]- -9,cA[58]+=cA[57]- -1562,cA[79]+=cA[38]- -971,cA[80]+=cA[68]- -1175;break;case-570:case cA[61]- -171:if(cA[65]<-(cA[65]+1274)){;break}cA[5]+=cA[39]-2034,cA[9]+=cA[41]- -409,cA[28]+=cA[35]- -1604,cA[30]+=cA[82]-866,cA[38]+=cA[75]-1087,cA[39]+=cA[61]- -5775,cA[58]+=cA[6]-231,cA[79]+=cA[13]- -234,cA[80]+=cA[30]-126;break;case cA[21]-619:cA[5]+=cA[32]-1727,cA[9]+=cA[52]-1302,cA[28]+=cA[40]-967,cA[30]+=cA[11]- -467,cA[38]+=cA[76]- -941,cA[39]+=cA[9]-110,cA[58]+=cA[50]- -166,cA[79]+=cA[73]- -4627,cA[80]+=cA[67]- -1019;break;case 643:case-738:cE["ba"]["d"].push((cE["ba"]["e"]|cE["ba"]["g"]<637){cA[5]+=cA[24]- -798,cA[9]+=cA[59]-821,cA[28]+=cA[63]- -1897,cA[30]+=cA[78]-954,cA[38]+=cA[11]-974,cA[39]+=cA[44]-544,cA[58]+=cA[46]- -390,cA[79]+=cA[17]- -1197,cA[80]+=cA[17]- -314;break}cA[5]+=cA[43]- -482,cA[9]+=cA[6]-412,cA[28]+=cA[82]-96,cA[30]+=cA[34]-874,cA[38]+=cA[74]-734,cA[39]+=cA[51]-2083,cA[58]+=cA[47]- -266,cA[79]+=cA[10]- -40,cA[80]+=cA[69]-1177;break;case cA[17]- -1600:cA[5]+=cA[36]-2000,cA[9]+=cA[59]- -854,cA[28]+=cA[65]-271,cA[30]+=cA[54]- -128,cA[38]+=cA[76]- -1393,cA[39]+=cA[47]-4449,cA[58]+=cA[30]- -56,cA[79]+=cA[59]- -546,cA[80]+=cA[43]-807;break;case 57:cE["ba"]["g"]=-1;for(cE["ba"]["h"]=0;cE["ba"]["h"]88?cA[45]+639:14;do{cE["ba"]["d"].push(cE["ba"]["e"]&cA[64]+-666);cE["ba"]["e"]>>=cA[14]+-97;cE["ba"]["f"]-=8}while(cE["ba"]["f"]>7);cE["ba"]["g"]=-1}}if(cE["ba"]["g"]>-1){cA[5]+=cA[35]-513,cA[9]+=cA[34]-969,cA[28]+=cA[29]-1288,cA[30]+=cA[73]- -247,cA[38]+=cA[12]-70,cA[39]+=cA[69]-581,cA[58]+=cA[77]- -1202,cA[79]+=cA[73]- -4162,cA[80]+=cA[17]-98;break}else{cA[5]+=cA[73]-741,cA[9]+=cA[67]- -1717,cA[28]+=cA[58]-821,cA[30]+=cA[27]- -1650,cA[38]+=cA[82]- -1059,cA[39]+=cA[41]- -2023,cA[58]+=cA[55]-349,cA[79]+=cA[64]-1794,cA[80]+=cA[54]-2349;break}}}}var cH;var cE=cA([...cD(0,5),589,...cD(6,9),-376,...cD(10,28),2356,-133,90,...cD(31,38),-288,704,...cD(40,58),-212,...cD(59,79),97,-532,...cD(81,84)]);if(cH){return cE}}function ef(cy,cA){return ee(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[ef(3020,10)]();var dZ=dl[dG+this[ef(3020,10)]()];dl[dG+dJ]=dZ/dl[dG+this[ef(3020,10)]()];break}case cz["cg"]["y"][dF(3035,4)]:{var dJ=this[dF(2578,10)]();var dZ=dl[dG+this[dF(2578,10)]()];dl[dG+dJ]=dZ%dl[dG+this[dF(2578,10)]()];break}case cz["cg"]["y"][dF(3041,4)]:{function eg(cy){function cA(cA,cE={["bc"]:{}},df){while(cC(cA)!==409){switch(cC(cA)){case-383:cE["bc"]["d"]=[];cE["bc"]["e"]=cA[43]+-846;cE["bc"]["f"]=cA[21]+-433;cA[4]+=cA[6]-286,cA[7]+=cA[3]-648,cA[24]+=cA[1]- -1028,cA[32]+=cA[10]-550,cA[50]+=cA[51]- -610,cA[68]+=cA[15]-565,cA[70]+=cA[4]-2212,cA[75]+=cA[5]- -1118;break;case 615:case-61:if(cA[cA[60]+-962]>cA[45]+1129){cA[4]+=cA[1]- -1392,cA[7]+=cA[72]- -749,cA[24]+=cA[29]- -249,cA[32]+=cA[70]-1036,cA[50]+=cA[19]-890,cA[68]+=cA[12]- -491,cA[70]+=cA[7]-539,cA[75]+=cA[66]- -492;break}case 859:case-577:cE["bc"]["a"]=cB(cA[49]+1000043,8595,91);cE["bc"]["b"]=""+(cy||"");cE["bc"]["c"]=cE["bc"]["b"].length;if(cA[cA[8]+593]==cA[66]+1395){cA[4]+=cA[7]-16,cA[7]+=cA[56]-234,cA[24]+=cA[8]- -4744,cA[32]+=cA[0]-1560,cA[50]+=cA[14]-1015,cA[68]+=cA[38]-1173,cA[70]+=cA[65]- -1933,cA[75]+=cA[8]- -476;break}cA[4]+=cA[34]- -198,cA[7]+=cA[62]-1661,cA[24]+=cA[56]- -769,cA[32]+=cA[29]- -1012,cA[50]+=cA[8]-816,cA[68]+=cA[1]-333,cA[70]+=cA[60]- -753,cA[75]+=cA[54]-1374;break;case 481:case 110:case cA[63]- -563:cE["bc"]["d"].push((cE["bc"]["e"]|cE["bc"]["g"]<88?13:cA[55]+-257;do{cE["bc"]["d"].push(cE["bc"]["e"]&cA[0]+-401);cE["bc"]["e"]>>=8;cE["bc"]["f"]-=cA[42]+887}while(cE["bc"]["f"]>cA[19]+67);cE["bc"]["g"]=-(cA[7]+613)}}if(cE["bc"]["g"]>-(cA[24]+-815)){cA[4]+=cA[50]-1126,cA[7]+=cA[47]- -830,cA[24]+=cA[48]-2292,cA[32]+=cA[12]-1504,cA[50]+=cA[20]-1295,cA[68]+=cA[3]- -1110,cA[70]+=cA[40]- -3456,cA[75]+=cA[43]-2668;break}else{cA[4]+=cA[71]-155,cA[7]+=cA[69]-3490,cA[24]+=cA[11]- -897,cA[32]+=cA[18]-353,cA[50]+=cA[61]- -695,cA[68]+=cA[59]- -193,cA[70]+=cA[13]- -2783;break}if(cA[cA[29]+203]<-1923){cA[4]+=cA[58]-384,cA[7]+=cA[4]- -747,cA[24]+=cA[1]-322,cA[32]+=cA[25]- -118,cA[50]+=cA[79]-42,cA[68]+=cA[0]- -203,cA[70]+=cA[19]- -1096,cA[75]+=cA[31]-760;break}if(cA[3]>cA[69]+5){cA[4]+=cA[30]-1528,cA[7]+=cA[45]-2280,cA[24]+=cA[21]-367,cA[32]+=cA[71]- -359,cA[50]+=cA[13]- -424,cA[68]+=cA[19]- -700,cA[70]+=cA[54]- -1540;break}case cA[61]- -1153:if(!(cA[cA[16]+613]>254)){cA[4]+=cA[57]-299,cA[7]+=cA[69]- -660,cA[24]+=cA[75]-1395,cA[32]+=cA[25]-451,cA[50]+=cA[62]-107,cA[68]+=cA[76]- -64,cA[70]+=cA[19]- -1255,cA[75]+=cA[3]-1170;break}cA[4]+=cA[54]-2297,cA[7]+=cA[13]-93,cA[24]+=cA[67]- -4075,cA[32]+=cA[43]-1925,cA[50]+=cA[71]- -945,cA[68]+=cA[34]-2114,cA[70]+=cA[77]- -450,cA[75]+=cA[57]- -943;break;case 233:case 771:cA[4]+=cA[37]- -702,cA[7]+=cA[70]- -2035,cA[24]+=cA[37]-3315,cA[32]+=cA[78]- -1030,cA[50]+=cA[53]- -996,cA[68]+=cA[77]- -2215,cA[70]+=cA[44]-1527,cA[75]+=cA[32]- -34;break;case 300:case 713:cA[4]+=cA[32]-667,cA[7]+=cA[59]-3890,cA[24]+=cA[23]- -1451,cA[32]+=cA[59]-294,cA[50]+=cA[73]- -1573,cA[68]+=cA[55]-1373,cA[70]+=cA[79]- -1642,cA[75]+=cA[68]- -1843;break;case-555:if(!(cA[cA[24]+193]>254)){cA[4]+=cA[68]- -891,cA[7]+=cA[52]- -42,cA[24]+=cA[15]-659,cA[32]+=cA[19]- -82,cA[50]+=cA[17]- -2029,cA[68]+=cA[31]- -2094,cA[70]+=cA[39]-991,cA[75]+=cA[18]-1927;break}cA[4]+=cA[77]- -558,cA[7]+=cA[25]-1247,cA[24]+=cA[78]- -4949,cA[32]+=cA[78]- -5,cA[50]+=cA[53]- -954,cA[68]+=cA[72]- -392,cA[70]+=cA[34]-2618,cA[75]+=cA[33]-1699;break;case cA[12]-892:return cH=true,(1,cz["cg"]["d"])(cE["bc"]["d"]);cA[4]+=cA[34]-264,cA[7]+=cA[49]- -4782,cA[24]+=cA[77]-32,cA[32]+=cA[26]-826,cA[50]+=cA[48]-1092,cA[68]+=cA[0]-437,cA[70]+=cA[49]-1027,cA[75]+=cA[73]-354;break}}}var cH;var cE=cA([...cD(0,4),-128,...cD(5,7),212,...cD(8,24),417,...cD(25,32),21,...cD(33,50),529,...cD(51,68),974,584,-2215,...cD(71,75),-79,...cD(76,80)]);if(cH){return cE}}function eh(cy,cA){return eg(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[eh(3052,10)]();var dZ=dl[dG+this[eh(3052,10)]()];dl[dG+dJ]=Math[eh(3066,4)](dZ,dl[dG+this[eh(3052,10)]()]);break}case cz["cg"]["y"][dF(3075,5)]:{function ei(cy){function cA(cA,cE={["be"]:{}},df){while(cC(cA)!==-595){switch(cC(cA)){case cA[46]-482:cA[12]+=cA[76]-202,cA[21]+=cA[55]-440,cA[23]+=cA[27]- -8654,cA[50]+=cA[76]-237,cA[61]+=cA[74]-2515,cA[79]+=cA[67]-420,cA[86]+=cA[0]-4918,cA[89]+=cA[7]-384;break;case cA[63]- -1448:cE["be"]["d"].push((cE["be"]["e"]|cE["be"]["g"]<cA[80]+-723?13:cA[42]+893;do{cE["be"]["d"].push(cE["be"]["e"]&cA[92]+-501);cE["be"]["e"]>>=8;cE["be"]["f"]-=8}while(cE["be"]["f"]>7);cE["be"]["g"]=-(cA[94]+-682)}}if(cE["be"]["g"]>-(cA[23]+780)){cA[12]+=cA[8]- -1916,cA[21]+=cA[39]-1621,cA[23]+=cA[40]- -1697,cA[50]+=cA[19]-3694,cA[61]+=cA[94]-548,cA[79]+=cA[55]-462,cA[86]+=cA[70]- -1098,cA[89]+=cA[10]- -724;break}else{cA[12]+=cA[78]- -1171,cA[21]+=cA[56]- -361,cA[23]+=cA[71]- -1430,cA[50]+=cA[5]-1295,cA[61]+=cA[56]- -1629,cA[79]+=cA[86]-4026,cA[86]+=cA[6]- -1824,cA[89]+=cA[80]- -550;break}if(!(cA[28]!=-(cA[74]+-553))){;break}case cA[2]- -1027:case-102:cA[12]+=cA[1]-228,cA[21]+=cA[12]- -6987,cA[23]+=cA[4]-183,cA[50]+=cA[61]-2461,cA[61]+=cA[0]-1889,cA[79]+=cA[56]- -455,cA[86]+=cA[62]-840,cA[89]+=cA[46]-1015;break;case 680:case 346:cE["be"]["a"]=cB(cA[58]+152833,8783,91);cE["be"]["b"]=""+(cy||"");cA[12]+=cA[28]-187,cA[21]+=cA[47]- -339,cA[23]+=cA[78]- -3462,cA[50]+=cA[49]- -1134,cA[61]+=cA[44]-1099,cA[79]+=cA[75]-1152,cA[86]+=cA[43]-3893,cA[89]+=cA[61]-12;break}}}var cH;var cE=cA([...cD(0,12),244,...cD(13,21),-501,630,-3957,...cD(24,50),-19,...cD(51,61),181,...cD(62,79),963,...cD(80,86),-695,...cD(87,89),-26,...cD(90,99)]);if(cH){return cE}}function ej(cy,cA){return ei(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[ej(3083,10)]();var dZ=dl[dG+this[ej(3083,10)]()];dl[dG+dJ]=dZ&dl[dG+this[ej(3083,10)]()];break}case cz["cg"]["y"][dF(3097,4)]:{function ek(cy){function cA(cA,cE={["bg"]:{}},df){while(cC(cA)!==-329){switch(cC(cA)){case 505:case cA[64]-699:cE["bg"]["a"]=cB(cA[31]+463443,8972,91);cE["bg"]["b"]=""+(cy||"");cA[6]+=cA[78]- -533,cA[18]+=cA[69]-646,cA[22]+=cA[78]- -729,cA[36]+=cA[33]-1448,cA[38]+=cA[34]-72,cA[48]+=cA[57]- -2318,cA[51]+=cA[16]-2199,cA[85]+=cA[0]-300;break;case 158:return cH=true,(1,cz["cg"]["d"])(cE["bg"]["d"]);cA[6]+=cA[36]- -869,cA[18]+=cA[56]- -907,cA[22]+=cA[49]- -233,cA[36]+=cA[32]-1090,cA[38]+=cA[3]-2121,cA[48]+=cA[27]- -1322,cA[51]+=cA[7]-334,cA[85]+=cA[11]- -1911;break;case 966:case-61:case cA[75]- -90:cA[6]+=cA[42]-1048,cA[18]+=cA[63]-125,cA[22]+=cA[81]- -1016,cA[36]+=cA[75]- -929,cA[38]+=cA[46]- -937,cA[48]+=cA[81]- -54,cA[51]+=cA[81]- -1082,cA[85]+=cA[62]-721;break;case cA[44]-713:case 111:cE["bg"]["f"]=cA[15]+343;cE["bg"]["g"]=-(cA[33]+-743);for(cE["bg"]["h"]=0;cE["bg"]["h"]cA[20]+-415?cA[2]+905:cA[63]+939;do{cE["bg"]["d"].push(cE["bg"]["e"]&cA[29]+388);cE["bg"]["e"]>>=cA[61]+618;cE["bg"]["f"]-=8}while(cE["bg"]["f"]>7);cE["bg"]["g"]=-1}}if(cE["bg"]["g"]>-1){cA[6]+=cA[1]-1289,cA[18]+=cA[48]- -2932,cA[22]+=cA[18]-4445,cA[36]+=cA[75]- -1547,cA[38]+=cA[57]- -1068,cA[48]+=cA[45]- -943,cA[51]+=cA[36]-1530,cA[85]+=cA[39]-848;break}else{cA[6]+=cA[43]-1913,cA[18]+=cA[27]- -1090,cA[22]+=cA[79]-590,cA[36]+=cA[52]- -790,cA[38]+=cA[87]- -1978,cA[48]+=cA[23]- -1239,cA[51]+=cA[68]- -1241,cA[85]+=cA[80]-1946;break}case-39:case 140:case 699:cE["bg"]["d"].push((cE["bg"]["e"]|cE["bg"]["g"]<cA[86]+257){cA[6]+=cA[63]-2063,cA[18]+=cA[60]-1135,cA[22]+=cA[16]- -2357,cA[36]+=cA[12]- -391,cA[38]+=cA[62]-1212,cA[48]+=cA[39]-580,cA[51]+=cA[28]- -1415,cA[85]+=cA[41]-1622;break}cA[6]+=cA[29]-1718,cA[18]+=cA[12]-1635,cA[22]+=cA[65]- -1773,cA[36]+=cA[13]- -908,cA[38]+=cA[39]-1843,cA[48]+=cA[7]-169,cA[51]+=cA[36]- -3248,cA[85]+=cA[21]-1530;break;case-158:case 236:if(cA[cA[50]+62]==cA[15]+851){cA[6]+=cA[65]- -247,cA[18]+=cA[0]- -340,cA[22]+=cA[52]-1221,cA[36]+=cA[4]-269,cA[38]+=cA[37]- -1814,cA[48]+=cA[59]- -115,cA[51]+=cA[16]- -1477,cA[85]+=cA[5]-1133;break}cA[6]+=cA[76]-2710,cA[18]+=cA[80]- -79,cA[22]+=cA[5]-330,cA[36]+=cA[31]- -178,cA[38]+=cA[68]- -1074,cA[48]+=cA[31]- -1396,cA[51]+=cA[13]- -808,cA[85]+=cA[0]-1977;break;case 553:case cA[33]-298:case 369:cE["bg"]["c"]=cE["bg"]["b"].length;cE["bg"]["d"]=[];cE["bg"]["e"]=cA[2]+892;cA[6]+=cA[54]- -877,cA[18]+=cA[28]- -727,cA[22]+=cA[20]-573,cA[36]+=cA[28]-672,cA[38]+=cA[86]-1467,cA[48]+=cA[63]-569,cA[51]+=cA[6]-982,cA[85]+=cA[16]- -607;break}}}var cH;var cE=cA([...cD(0,6),-987,...cD(7,18),-515,...cD(19,22),381,...cD(23,36),296,-898,-70,...cD(39,48),-609,...cD(49,51),2615,...cD(52,85),525,...cD(86,88)]);if(cH){return cE}}function el(cy,cA){return ek(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[el(3107,10)]();var dZ=dl[dG+this[el(3107,10)]()];dl[dG+dJ]=dZ|dl[dG+this[el(3107,10)]()];break}case cz["cg"]["y"][dF(3121,5)]:{function em(cy){function cA(cA,cE={["bi"]:{}},df){while(cC(cA)!==509){switch(cC(cA)){case-363:cA[0]+=cA[47]- -181,cA[16]+=cA[1]- -1247,cA[29]+=cA[78]-284,cA[36]+=cA[37]- -1036,cA[38]+=cA[19]- -41,cA[53]+=cA[24]-1043,cA[56]+=cA[14]-324,cA[60]+=cA[47]- -2201,cA[74]+=cA[4]- -1417,cA[75]+=cA[4]-2811;break;case 351:case 942:cE["bi"]["d"].push((cE["bi"]["e"]|cE["bi"]["g"]<-(cA[38]+1525)){cA[0]+=cA[54]-205,cA[16]+=cA[31]- -115,cA[29]+=cA[41]-294,cA[36]+=cA[71]- -351,cA[38]+=cA[5]-369,cA[53]+=cA[33]-781,cA[56]+=cA[9]-842,cA[60]+=cA[49]-654,cA[74]+=cA[52]-1686,cA[75]+=cA[13]- -2890;break}cA[0]+=cA[63]- -2002,cA[16]+=cA[8]- -3759,cA[29]+=cA[5]-1500,cA[36]+=cA[47]-850,cA[38]+=cA[31]- -1860,cA[53]+=cA[39]-1981,cA[56]+=cA[51]-1952,cA[60]+=cA[14]-818,cA[74]+=cA[75]-686,cA[75]+=cA[70]-1339;break;case cA[66]- -257:cE["bi"]["f"]=0;cE["bi"]["g"]=-(cA[53]+322);for(cE["bi"]["h"]=0;cE["bi"]["h"]cA[27]+784?cA[13]+352:cA[33]+-730;do{cE["bi"]["d"].push(cE["bi"]["e"]&cA[44]+53);cE["bi"]["e"]>>=cA[49]+389;cE["bi"]["f"]-=8}while(cE["bi"]["f"]>cA[32]+-909);cE["bi"]["g"]=-(cA[50]+17)}}if(cE["bi"]["g"]>-1){cA[0]+=cA[78]-43,cA[16]+=cA[57]- -483,cA[29]+=cA[22]-1566,cA[36]+=cA[58]-74,cA[38]+=cA[27]-1150,cA[53]+=cA[25]- -816,cA[56]+=cA[5]- -289,cA[60]+=cA[2]- -3621,cA[74]+=cA[67]- -564,cA[75]+=cA[37]- -48;break}else{cA[0]+=cA[72]-1602,cA[16]+=cA[14]-902,cA[29]+=cA[56]- -550,cA[36]+=cA[72]- -300,cA[38]+=cA[17]-1000,cA[53]+=cA[74]- -1312,cA[56]+=cA[71]- -1237,cA[60]+=cA[53]- -747,cA[74]+=cA[52]- -1708,cA[75]+=cA[67]- -61;break}case-411:cA[0]+=cA[57]- -1255,cA[16]+=cA[40]- -203,cA[29]+=cA[44]- -1445,cA[36]+=cA[65]- -254,cA[38]+=cA[36]-1940,cA[53]+=cA[52]-118,cA[56]+=cA[57]- -2450,cA[60]+=cA[47]-2230,cA[74]+=cA[29]-1974,cA[75]+=cA[67]- -316;break;case 312:case-685:case cA[18]-1004:cE["bi"]["a"]=cB(cA[12]+997876,9156,91);cE["bi"]["b"]=""+(cy||"");cE["bi"]["c"]=cE["bi"]["b"].length;if(cA[33]!=744){cA[0]+=cA[61]-657,cA[16]+=cA[76]-685,cA[29]+=cA[9]-406,cA[36]+=cA[58]- -511,cA[38]+=cA[51]-2197,cA[53]+=cA[24]- -450,cA[56]+=cA[74]- -1096,cA[60]+=cA[16]-173,cA[74]+=cA[62]- -507,cA[75]+=cA[23]- -1311;break}cA[0]+=cA[64]-1995,cA[16]+=cA[22]-1205,cA[29]+=cA[10]- -1467,cA[36]+=cA[0]- -857,cA[38]+=cA[58]-1377,cA[53]+=cA[78]- -104,cA[56]+=cA[23]- -1562,cA[60]+=cA[48]-825,cA[74]+=cA[71]- -971,cA[75]+=cA[32]-655;break;case cA[60]-928:case-885:cE["bi"]["d"]=[];cE["bi"]["e"]=cA[25]+485;cA[0]+=cA[44]- -1179,cA[16]+=cA[29]-1114,cA[29]+=cA[14]-1565,cA[36]+=cA[46]- -239,cA[38]+=cA[10]- -1585,cA[53]+=cA[73]- -1396,cA[56]+=cA[61]-313,cA[60]+=cA[18]-1435,cA[74]+=cA[43]-1496,cA[75]+=cA[70]-66;break}}}var cH;var cE=cA([627,...cD(1,16),10,...cD(17,29),413,...cD(30,36),81,-898,377,...cD(39,53),-958,...cD(54,56),103,...cD(57,60),833,...cD(61,74),-493,-517,...cD(76,80)]);if(cH){return cE}}function en(cy,cA){return em(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[dF(2578,10)]();var dZ=dl[dG+this[en(3127,10)]()];dl[dG+dJ]=dZ^dl[dG+this[en(3127,10)]()];break}case cz["cg"]["y"][dF(3140,4)]:{function eo(cy){function cA(cA,cE={["bk"]:{}},df){while(cC(cA)!==-155){switch(cC(cA)){case cA[69]- -58:cE["bk"]["e"]=cA[13]+339;cE["bk"]["f"]=cA[5]+-331;cA[9]+=cA[49]- -890,cA[14]+=cA[66]-53,cA[17]+=cA[76]-504,cA[22]+=cA[18]-639,cA[33]+=cA[62]- -448,cA[44]+=cA[15]- -367,cA[50]+=cA[64]- -1174,cA[55]+=cA[46]-900,cA[79]+=cA[58]-790,cA[87]+=cA[26]-1802;break;case 835:cA[9]+=cA[23]-469,cA[14]+=cA[86]-109,cA[17]+=cA[23]- -5418,cA[22]+=cA[16]-842,cA[33]+=cA[87]- -685,cA[44]+=cA[53]-964,cA[50]+=cA[18]-425,cA[55]+=cA[50]- -1004,cA[79]+=cA[33]-1026,cA[87]+=cA[46]- -124;break;case 277:cE["bk"]["a"]=cB(cA[50]+419172,9343,91);cE["bk"]["b"]=""+(cy||"");cE["bk"]["c"]=cE["bk"]["b"].length;cE["bk"]["d"]=[];cA[9]+=cA[21]-527,cA[14]+=cA[72]- -209,cA[17]+=cA[35]- -143,cA[22]+=cA[34]-647,cA[33]+=cA[22]-1797,cA[44]+=cA[68]-356,cA[50]+=cA[31]- -401,cA[55]+=cA[45]- -1297,cA[79]+=cA[39]-1090,cA[87]+=cA[36]- -119;break;case cA[38]- -1119:cE["bk"]["d"].push((cE["bk"]["e"]|cE["bk"]["g"]<88?cA[4]+678:14;do{cE["bk"]["d"].push(cE["bk"]["e"]&cA[19]+315);cE["bk"]["e"]>>=8;cE["bk"]["f"]-=cA[4]+673}while(cE["bk"]["f"]>cA[7]+616);cE["bk"]["g"]=-1}}if(cE["bk"]["g"]>-(cA[70]+-295)){cA[9]+=cA[28]-190,cA[14]+=cA[84]-526,cA[17]+=cA[53]- -5150,cA[22]+=cA[8]-866,cA[33]+=cA[26]-1962,cA[44]+=cA[72]-635,cA[50]+=cA[83]-1959,cA[55]+=cA[22]- -1668,cA[79]+=cA[23]- -22,cA[87]+=cA[38]- -35;break}else{cA[9]+=cA[21]-184,cA[14]+=cA[44]-524,cA[17]+=cA[83]- -199,cA[22]+=cA[36]-2191,cA[33]+=cA[57]-910,cA[44]+=cA[13]- -1510,cA[50]+=cA[73]-445,cA[55]+=cA[51]-1193,cA[79]+=cA[66]- -1500,cA[87]+=cA[35]- -771;break}case cA[74]-466:cA[9]+=cA[12]-1022,cA[14]+=cA[45]-777,cA[17]+=cA[45]- -737,cA[22]+=cA[44]- -1451,cA[33]+=cA[54]-1177,cA[44]+=cA[48]-677,cA[50]+=cA[65]- -301,cA[55]+=cA[46]- -1670,cA[79]+=cA[5]-882,cA[87]+=cA[18]- -456;break;case-587:return cH=true,(1,cz["cg"]["d"])(cE["bk"]["d"]);if(!(cA[40]<-(cA[13]+1046))){cA[9]+=cA[55]-167,cA[14]+=cA[41]- -1882,cA[17]+=cA[46]-290,cA[22]+=cA[47]- -965,cA[33]+=cA[24]- -447,cA[44]+=cA[23]- -161,cA[50]+=cA[18]-1167,cA[55]+=cA[3]-55,cA[79]+=cA[2]- -421,cA[87]+=cA[53]-328;break}cA[9]+=cA[2]- -208,cA[14]+=cA[70]- -1383,cA[17]+=cA[32]-1901,cA[22]+=cA[71]- -3453,cA[33]+=cA[51]-186,cA[44]+=cA[84]-935,cA[50]+=cA[4]-528,cA[55]+=cA[13]- -494,cA[79]+=cA[44]-2014,cA[87]+=cA[78]-321;break;case 317:if(cA[cA[59]+-429]>184){cA[9]+=cA[35]- -821,cA[14]+=cA[5]-4415,cA[17]+=cA[32]- -803,cA[22]+=cA[3]-1718,cA[33]+=cA[60]-2041,cA[44]+=cA[66]- -312,cA[50]+=cA[32]-286,cA[55]+=cA[76]-35,cA[79]+=cA[24]- -759,cA[87]+=cA[42]- -1729;break}cA[9]+=cA[85]- -1303,cA[14]+=cA[21]-4517,cA[17]+=cA[5]- -1388,cA[22]+=cA[24]-1928,cA[33]+=cA[55]-506,cA[44]+=cA[53]-1,cA[50]+=cA[45]- -1256,cA[55]+=cA[77]- -899,cA[79]+=cA[36]- -681,cA[87]+=cA[46]- -953;break;case-469:if(!(cA[cA[70]+-222]>cA[6]+213)){cA[9]+=cA[66]- -674,cA[14]+=cA[9]-708,cA[17]+=cA[56]- -1523,cA[22]+=cA[61]- -532,cA[33]+=cA[66]- -426,cA[44]+=cA[52]- -32,cA[50]+=cA[64]-1206,cA[55]+=cA[5]-644,cA[79]+=cA[76]- -955,cA[87]+=cA[30]-1303;break}cA[9]+=cA[47]- -652,cA[14]+=cA[22]- -1287,cA[17]+=cA[26]-467,cA[22]+=cA[74]- -413,cA[33]+=cA[64]-605,cA[44]+=cA[68]-539,cA[50]+=cA[84]-805,cA[55]+=cA[20]-1246,cA[79]+=cA[25]- -1342,cA[87]+=cA[9]-1967;break}}}var cH;var cE=cA([...cD(0,9),-834,...cD(10,14),186,...cD(15,17),407,...cD(18,22),574,...cD(23,33),325,...cD(34,44),223,...cD(45,50),-552,...cD(51,55),37,...cD(56,79),498,...cD(80,87),-299]);if(cH){return cE}}function ep(cy,cA){return eo(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[dF(2578,10)]();var dZ=dl[dG+this[dF(2578,10)]()];dl[dG+dJ]=dZ<-(cA[84]+143)){cA[3]+=cA[32]-712,cA[26]+=cA[13]- -815,cA[30]+=cA[8]- -1656,cA[40]+=cA[77]- -64,cA[53]+=cA[36]-707,cA[54]+=cA[27]- -206,cA[55]+=cA[43]-2630,cA[56]+=cA[40]- -1143,cA[65]+=cA[12]- -277;break}cA[3]+=cA[40]-204,cA[26]+=cA[84]-998,cA[30]+=cA[8]- -1269,cA[40]+=cA[29]-1266,cA[53]+=cA[54]-1732,cA[54]+=cA[39]-1567,cA[55]+=cA[31]- -530,cA[56]+=cA[78]- -932,cA[65]+=cA[34]- -417;break;case 239:case 195:cA[3]+=cA[79]- -468,cA[26]+=cA[17]- -1080,cA[30]+=cA[28]-1194,cA[40]+=cA[52]- -3,cA[53]+=cA[45]- -1127,cA[54]+=cA[42]- -414,cA[55]+=cA[50]-369,cA[56]+=cA[80]-556,cA[65]+=cA[22]- -17;break;case-461:case cA[53]- -29:cE["bm"]["c"]=cE["bm"]["b"].length;cE["bm"]["d"]=[];cE["bm"]["e"]=cA[27]+696;cA[3]+=cA[17]- -1519,cA[26]+=cA[53]- -1401,cA[30]+=cA[70]-1762,cA[40]+=cA[21]-1129,cA[53]+=cA[49]- -475,cA[54]+=cA[73]- -450,cA[55]+=cA[72]- -313,cA[56]+=cA[64]-890,cA[65]+=cA[72]- -1106;break;case 563:cA[3]+=cA[7]- -539,cA[26]+=cA[81]-962,cA[30]+=cA[60]- -19,cA[40]+=cA[47]- -501,cA[53]+=cA[41]- -593,cA[54]+=cA[29]- -319,cA[55]+=cA[35]-643,cA[56]+=cA[77]- -1089,cA[65]+=cA[22]-2074;break;case 957:if(!(cA[cA[15]+360]<498)){cA[3]+=cA[75]- -1416,cA[26]+=cA[44]-433,cA[30]+=cA[2]- -1009,cA[40]+=cA[30]-1921,cA[53]+=cA[46]- -397,cA[54]+=cA[53]-99,cA[55]+=cA[18]- -1763,cA[56]+=cA[1]-623,cA[65]+=cA[30]-3204;break}cA[3]+=cA[32]- -737,cA[26]+=cA[36]-1108,cA[30]+=cA[46]- -220,cA[40]+=cA[65]-1019,cA[53]+=cA[6]- -235,cA[54]+=cA[4]- -1052,cA[55]+=cA[69]- -1363,cA[56]+=cA[6]-1169,cA[65]+=cA[71]-1865;break;case 653:case-641:cE["bm"]["f"]=0;cE["bm"]["g"]=-(cA[57]+737);cA[3]+=cA[16]-218,cA[26]+=cA[64]- -1304,cA[30]+=cA[24]-876,cA[40]+=cA[60]-836,cA[53]+=cA[41]- -2545,cA[54]+=cA[1]- -224,cA[55]+=cA[68]-710,cA[56]+=cA[12]-363,cA[65]+=cA[38]-857;break;case 871:case-295:cE["bm"]["a"]=cB(cA[6]+692286,9537,91);cE["bm"]["b"]=""+(cy||"");cA[3]+=cA[26]-1226,cA[26]+=cA[42]- -557,cA[30]+=cA[48]- -1663,cA[40]+=cA[17]- -32,cA[53]+=cA[45]- -138,cA[54]+=cA[55]- -995,cA[55]+=cA[18]- -509,cA[56]+=cA[22]- -395,cA[65]+=cA[27]-811;break;case cA[67]- -497:return cH=true,(1,cz["cg"]["d"])(cE["bm"]["d"]);if(!(cA[53]==cA[4]+1151)){;break}cA[3]+=cA[81]-585,cA[26]+=cA[78]-490,cA[30]+=cA[15]-161,cA[40]+=cA[78]- -616,cA[53]+=cA[84]-2079,cA[54]+=cA[29]-444,cA[55]+=cA[59]-411,cA[56]+=cA[69]-23,cA[65]+=cA[56]- -3019;break;case 6:case 96:cA[3]+=cA[20]-1400,cA[26]+=cA[45]- -2836,cA[30]+=cA[38]- -922,cA[40]+=cA[76]-847,cA[53]+=cA[43]-1427,cA[54]+=cA[70]-174,cA[55]+=cA[48]-974,cA[56]+=cA[25]- -422,cA[65]+=cA[27]-322;break;case 343:for(cE["bm"]["h"]=cA[6]+-59;cE["bm"]["h"]cA[19]+148?cA[45]+639:14;do{cE["bm"]["d"].push(cE["bm"]["e"]&255);cE["bm"]["e"]>>=cA[72]+-20;cE["bm"]["f"]-=8}while(cE["bm"]["f"]>7);cE["bm"]["g"]=-(cA[21]+-432)}}if(cE["bm"]["g"]>-1){cA[3]+=cA[32]- -590,cA[26]+=cA[64]-2114,cA[30]+=cA[58]-848,cA[40]+=cA[43]-131,cA[53]+=cA[15]-72,cA[54]+=cA[80]-655,cA[55]+=cA[21]-267,cA[56]+=cA[21]-820,cA[65]+=cA[5]-81;break}else{cA[3]+=cA[79]- -2390,cA[26]+=cA[42]-1259,cA[30]+=cA[45]- -1515,cA[40]+=cA[24]-1002,cA[53]+=cA[2]- -494,cA[54]+=cA[84]- -682,cA[55]+=cA[84]- -1056,cA[56]+=cA[58]-1348,cA[65]+=cA[9]-2667;break}}}}var cH;var cE=cA([...cD(0,3),725,...cD(4,26),-138,...cD(27,30),-479,...cD(31,40),612,...cD(41,53),-488,-242,-871,-633,...cD(57,65),640,...cD(66,86)]);if(cH){return cE}}function er(cy,cA){return eq(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[dF(2578,10)]();var dZ=dl[dG+this[er(3171,10)]()];dl[dG+dJ]=dZ>>dl[dG+this[er(3171,10)]()];break}case cz["cg"]["y"][dF(3185,5)]:{var dJ=this[dF(2578,10)]();var dZ=dl[dG+this[dF(2578,10)]()];dl[dG+dJ]=dZ>>>dl[dG+this[dF(2578,10)]()];break}case cz["cg"]["y"]["LT"]:{function es(cy){function cA(cA,cE={["bo"]:{}},df){while(cC(cA)!==476){switch(cC(cA)){case-843:case-407:cA[0]+=cA[33]-1641,cA[13]+=cA[80]-475,cA[17]+=cA[67]-1380,cA[25]+=cA[12]- -4443,cA[35]+=cA[3]-482,cA[40]+=cA[62]-1201,cA[61]+=cA[36]-1046,cA[63]+=cA[10]-898,cA[74]+=cA[50]- -266,cA[75]+=cA[14]- -262,cA[80]+=cA[33]-1418;break;case-571:case-266:cE["bo"]["c"]=cE["bo"]["b"].length;cE["bo"]["d"]=[];cA[0]+=cA[32]-1512,cA[13]+=cA[23]- -431,cA[17]+=cA[60]-1602,cA[25]+=cA[62]-60,cA[35]+=cA[0]-718,cA[40]+=cA[42]-50,cA[61]+=cA[24]-627,cA[63]+=cA[77]- -1899,cA[74]+=cA[0]- -2952,cA[75]+=cA[22]- -226,cA[80]+=cA[18]-1374;break;case cA[55]-74:case-743:cE["bo"]["e"]=cA[4]+665;cE["bo"]["f"]=cA[14]+-105;cA[0]+=cA[21]-307,cA[13]+=cA[28]- -149,cA[17]+=cA[63]- -909,cA[25]+=cA[73]-1113,cA[35]+=cA[58]- -1207,cA[40]+=cA[24]-446,cA[61]+=cA[0]-1373,cA[63]+=cA[32]-1021,cA[74]+=cA[61]- -1749,cA[75]+=cA[6]-504,cA[80]+=cA[23]- -1799;break;case cA[21]-646:case-374:cE["bo"]["d"].push((cE["bo"]["e"]|cE["bo"]["g"]<88?cA[14]+-92:14;do{cE["bo"]["d"].push(cE["bo"]["e"]&255);cE["bo"]["e"]>>=8;cE["bo"]["f"]-=cA[55]+-263}while(cE["bo"]["f"]>7);cE["bo"]["g"]=-1}}if(cE["bo"]["g"]>-(cA[52]+99)){cA[0]+=cA[51]- -163,cA[13]+=cA[5]-4102,cA[17]+=cA[40]-842,cA[25]+=cA[57]- -1085,cA[35]+=cA[15]- -268,cA[40]+=cA[27]- -583,cA[61]+=cA[48]- -1930,cA[63]+=cA[75]- -1169,cA[74]+=cA[11]- -211,cA[75]+=cA[45]- -1934,cA[80]+=cA[41]-221;break}else{cA[0]+=cA[48]-533,cA[13]+=cA[40]- -896,cA[17]+=cA[52]-540,cA[25]+=cA[14]- -1411,cA[35]+=cA[76]- -38,cA[40]+=cA[79]- -17,cA[61]+=cA[19]- -2084,cA[63]+=cA[24]-3114,cA[74]+=cA[19]-434,cA[75]+=cA[0]- -163,cA[80]+=cA[29]- -902;break}case-630:case cA[34]-661:cA[0]+=cA[18]- -649,cA[13]+=cA[16]-3099,cA[17]+=cA[57]- -277,cA[25]+=cA[9]-1474,cA[35]+=cA[60]-1196,cA[40]+=cA[51]-1866,cA[61]+=cA[39]-165,cA[63]+=cA[79]- -1190,cA[74]+=cA[31]- -181,cA[75]+=cA[64]-897,cA[80]+=cA[71]- -3877;break;case 767:case 79:case 885:return cH=true,(1,cz["cg"]["d"])(cE["bo"]["d"]);cA[0]+=cA[46]- -772,cA[13]+=cA[19]-727,cA[17]+=cA[23]-7,cA[25]+=cA[54]-1597,cA[35]+=cA[18]-3400,cA[40]+=cA[59]- -444,cA[61]+=cA[36]-371,cA[63]+=cA[49]- -3111,cA[74]+=cA[30]-1416,cA[75]+=cA[41]- -2055,cA[80]+=cA[1]- -25;break;case 210:case 252:case cA[60]-1182:cE["bo"]["a"]=cB(cA[16]+242992,9726,91);cE["bo"]["b"]=""+(cy||"");if(!(cA[cA[35]+2123]!=cA[73]+1235)){cA[0]+=cA[76]-1092,cA[13]+=cA[27]- -371,cA[17]+=cA[41]- -1204,cA[25]+=cA[22]-1005,cA[35]+=cA[41]- -3589,cA[40]+=cA[68]- -1352,cA[61]+=cA[79]-2035,cA[63]+=cA[31]- -109,cA[74]+=cA[31]- -335,cA[75]+=cA[73]- -1345,cA[80]+=cA[77]- -1007;break}cA[0]+=cA[33]-1275,cA[13]+=cA[8]- -244,cA[17]+=cA[70]-249,cA[25]+=cA[48]- -358,cA[35]+=cA[28]- -2352,cA[40]+=cA[66]- -2072,cA[61]+=cA[9]-2095,cA[63]+=cA[11]-342,cA[74]+=cA[51]-3278,cA[75]+=cA[60]-785,cA[80]+=cA[6]- -241;break;case-829:cA[0]+=cA[74]-328,cA[13]+=cA[77]- -712,cA[17]+=cA[76]- -145,cA[25]+=cA[63]- -662,cA[35]+=cA[17]-147,cA[40]+=cA[19]- -440,cA[61]+=cA[69]-1521,cA[63]+=cA[58]-322,cA[74]+=cA[1]- -1428,cA[75]+=cA[39]-1094,cA[80]+=cA[74]- -1294;break;case-423:case 801:if(cA[59]==cA[31]+828){cA[0]+=cA[72]- -1450,cA[13]+=cA[43]-4127,cA[17]+=cA[76]- -642,cA[25]+=cA[45]-290,cA[35]+=cA[11]- -4582,cA[40]+=cA[75]-703,cA[61]+=cA[76]- -414,cA[63]+=cA[38]- -328,cA[74]+=cA[6]-1438,cA[75]+=cA[63]-34,cA[80]+=cA[51]-2022;break}cA[0]+=cA[17]- -925,cA[13]+=cA[49]- -1902,cA[17]+=cA[24]- -3,cA[25]+=cA[64]-670,cA[35]+=cA[43]- -3109,cA[40]+=cA[77]-225,cA[61]+=cA[50]- -334,cA[63]+=cA[76]-3062,cA[74]+=cA[43]-2099,cA[75]+=cA[27]-361,cA[80]+=cA[56]- -1649;break}}}var cH;var cE=cA([606,...cD(1,13),194,...cD(14,17),162,...cD(18,25),-570,...cD(26,35),-2114,...cD(36,40),-962,...cD(41,61),830,896,206,...cD(64,74),119,-992,...cD(76,80),23,-485]);if(cH){return cE}}function et(cy,cA){return es(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[dF(2578,10)]();var dZ=dl[dG+this[et(3194,10)]()];dl[dG+dJ]=dZcA[64]+-833?13:14;do{cE["bq"]["d"].push(cE["bq"]["e"]&cA[76]+164);cE["bq"]["e"]>>=cA[72]+-20;cE["bq"]["f"]-=8}while(cE["bq"]["f"]>7);cE["bq"]["g"]=-(cA[74]+-880)}}if(cE["bq"]["g"]>-1){cA[4]+=cA[79]- -181,cA[7]+=cA[28]- -80,cA[9]+=cA[26]-498,cA[28]+=cA[46]-1095,cA[68]+=cA[80]-317,cA[69]+=cA[53]- -1334,cA[75]+=cA[1]-2441,cA[85]+=cA[0]- -2001,cA[89]+=cA[25]- -832;break}else{cA[4]+=cA[54]-2038,cA[7]+=cA[58]-1209,cA[9]+=cA[33]-686,cA[28]+=cA[71]- -404,cA[68]+=cA[33]-1442,cA[69]+=cA[28]-1046,cA[75]+=cA[33]- -915,cA[85]+=cA[35]- -2212,cA[89]+=cA[51]-602;break}if(cA[22]<-438){cA[4]+=cA[48]-442,cA[7]+=cA[60]-1358,cA[9]+=cA[57]- -754,cA[28]+=cA[82]- -210,cA[68]+=cA[61]-366,cA[69]+=cA[51]-1653,cA[75]+=cA[58]-564,cA[85]+=cA[57]- -3244,cA[89]+=cA[9]- -466;break}case 774:return cH=true,(1,cz["cg"]["d"])(cE["bq"]["d"]);cA[4]+=cA[2]- -1927,cA[7]+=cA[52]- -180,cA[9]+=cA[56]- -1082,cA[28]+=cA[70]- -215,cA[68]+=cA[78]-154,cA[69]+=cA[29]-1261,cA[75]+=cA[29]-2241,cA[85]+=cA[32]-159,cA[89]+=cA[27]- -1047;break;case cA[7]-156:cA[4]+=cA[81]-678,cA[7]+=cA[89]-501,cA[9]+=cA[78]- -17,cA[28]+=cA[7]- -144,cA[68]+=cA[12]-283,cA[69]+=cA[70]-239,cA[75]+=cA[22]- -949,cA[85]+=cA[46]- -1658,cA[89]+=cA[10]-3322;break;case 537:cA[4]+=cA[62]-404,cA[7]+=cA[49]-974,cA[9]+=cA[30]-316,cA[28]+=cA[39]- -761,cA[68]+=cA[10]-1794,cA[69]+=cA[9]-1495,cA[75]+=cA[65]- -909,cA[85]+=cA[53]- -195,cA[89]+=cA[43]-175;break;case-720:case-788:case-843:cA[4]+=cA[22]-823,cA[7]+=cA[12]-782,cA[9]+=cA[72]- -848,cA[28]+=cA[83]- -371,cA[68]+=cA[37]- -397,cA[69]+=cA[13]-233,cA[75]+=cA[41]- -356,cA[85]+=cA[51]- -81,cA[89]+=cA[13]- -1082;break;case cA[57]- -698:if(cA[cA[80]+-789]<-739){cA[4]+=cA[7]- -2418,cA[7]+=cA[26]-1935,cA[9]+=cA[18]- -536,cA[28]+=cA[23]- -109,cA[68]+=cA[49]-156,cA[69]+=cA[55]-1293,cA[75]+=cA[23]- -230,cA[85]+=cA[21]-653,cA[89]+=cA[22]-703;break}cA[4]+=cA[55]- -4389,cA[7]+=cA[69]-2480,cA[9]+=cA[83]-695,cA[28]+=cA[74]-917,cA[68]+=cA[87]-335,cA[69]+=cA[22]-3840,cA[75]+=cA[3]-1413,cA[85]+=cA[58]- -204,cA[89]+=cA[47]- -423;break;case 793:case-287:case 819:cE["bq"]["e"]=cA[25]+485;cE["bq"]["f"]=cA[67]+-14;cE["bq"]["g"]=-(cA[17]+790);cA[4]+=cA[50]- -177,cA[7]+=cA[12]-26,cA[9]+=cA[5]-349,cA[28]+=cA[9]-5,cA[68]+=cA[84]- -349,cA[69]+=cA[6]- -603,cA[75]+=cA[45]- -827,cA[85]+=cA[69]-2241,cA[89]+=cA[86]-4;break;case 440:cE["bq"]["c"]=cE["bq"]["b"].length;cE["bq"]["d"]=[];if(!(cA[cA[54]+-885]<750)){cA[4]+=cA[79]- -2036,cA[7]+=cA[66]- -1165,cA[9]+=cA[27]- -2300,cA[28]+=cA[29]-8,cA[68]+=cA[47]-728,cA[69]+=cA[44]-143,cA[75]+=cA[16]-120,cA[85]+=cA[51]-3062,cA[89]+=cA[7]-981;break}cA[4]+=cA[30]- -346,cA[7]+=cA[42]- -1269,cA[9]+=cA[16]- -2235,cA[28]+=cA[75]-860,cA[68]+=cA[73]-442,cA[69]+=cA[41]- -176,cA[75]+=cA[78]-808,cA[85]+=cA[59]-10,cA[89]+=cA[25]-201;break;case cA[39]-1584:case 205:case-154:cA[4]+=cA[74]-969,cA[7]+=cA[38]- -3745,cA[9]+=cA[40]- -501,cA[28]+=cA[73]-404,cA[68]+=cA[54]-1116,cA[69]+=cA[54]-392,cA[75]+=cA[59]-2887,cA[85]+=cA[81]- -1807,cA[89]+=cA[52]- -564;break}}}var cH;var cE=cA([...cD(0,4),-638,...cD(5,7),-361,-531,750,...cD(10,28),240,...cD(29,68),-659,-190,...cD(70,75),-164,...cD(76,85),-60,...cD(86,89),-220]);if(cH){return cE}}function ev(cy,cA){return eu(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[ev(3208,10)]();var dZ=dl[dG+this[ev(3208,10)]()];dl[dG+dJ]=dZ>dl[dG+this[ev(3208,10)]()];break}case cz["cg"]["y"][dF(3222,4)]:{var dJ=this[dF(2578,10)]();var dZ=dl[dG+this[dF(2578,10)]()];dl[dG+dJ]=dZ<=dl[dG+this[dF(2578,10)]()];break}case cz["cg"]["y"][dF(3228,4)]:{var dJ=this[dF(2578,10)]();var dZ=dl[dG+this[dF(2578,10)]()];dl[dG+dJ]=dZ>=dl[dG+this[dF(2578,10)]()];break}case cz["cg"]["y"]["EQ"]:{var dJ=this[dF(2578,10)]();var dZ=dl[dG+this[dF(2578,10)]()];dl[dG+dJ]=dZ===dl[dG+this[dF(2578,10)]()];break}case cz["cg"]["y"][dF(3239,4)]:{function ew(cy){function cA(cA,cE={["bs"]:{}},df){while(cC(cA)!==756){switch(cC(cA)){case cA[74]-1443:return cH=true,(1,cz["cg"]["d"])(cE["bs"]["d"]);cA[3]+=cA[70]-754,cA[15]+=cA[74]- -1935,cA[24]+=cA[81]-742,cA[28]+=cA[69]-204,cA[39]+=cA[17]- -1,cA[45]+=cA[24]- -1265,cA[53]+=cA[66]- -777,cA[66]+=cA[12]-1521,cA[68]+=cA[29]- -440,cA[83]+=cA[69]- -335,cA[85]+=cA[11]- -58;break;case-781:cA[3]+=cA[81]- -432,cA[15]+=cA[8]-358,cA[24]+=cA[70]- -2734,cA[28]+=cA[12]- -345,cA[39]+=cA[54]-1016,cA[45]+=cA[5]-1938,cA[53]+=cA[0]-2481,cA[66]+=cA[22]- -546,cA[68]+=cA[51]-1406,cA[83]+=cA[50]- -378,cA[85]+=cA[25]- -336;break;case cA[64]- -24:cA[3]+=cA[49]- -653,cA[15]+=cA[20]-334,cA[24]+=cA[84]-592,cA[28]+=cA[0]-131,cA[39]+=cA[62]-5519,cA[45]+=cA[35]- -378,cA[53]+=cA[61]- -282,cA[66]+=cA[54]-825,cA[68]+=cA[65]- -1216,cA[83]+=cA[64]-1486,cA[85]+=cA[12]- -1514;break;case-449:if(!(cA[cA[10]+-3]!=-(cA[56]+1515))){cA[3]+=cA[54]-857,cA[15]+=cA[19]- -228,cA[24]+=cA[40]- -1413,cA[28]+=cA[36]-2221,cA[39]+=cA[52]- -1832,cA[45]+=cA[60]-1174,cA[53]+=cA[34]-1062,cA[66]+=cA[86]-799,cA[68]+=cA[49]- -721,cA[83]+=cA[5]-151,cA[85]+=cA[86]- -346;break}if(!(cA[cA[4]+740]==237)){cA[3]+=cA[68]-505,cA[15]+=cA[69]-362,cA[24]+=cA[38]- -1554,cA[28]+=cA[78]-202,cA[39]+=cA[18]- -839,cA[45]+=cA[19]-443,cA[53]+=cA[68]- -1731,cA[66]+=cA[45]- -489,cA[68]+=cA[32]-687,cA[83]+=cA[63]- -1253,cA[85]+=cA[60]-2557;break}cA[3]+=cA[23]-741,cA[15]+=cA[67]- -208,cA[24]+=cA[68]- -2336,cA[28]+=cA[8]- -203,cA[39]+=cA[26]- -395,cA[45]+=cA[86]-274,cA[53]+=cA[85]- -119,cA[66]+=cA[18]- -274,cA[68]+=cA[8]- -760,cA[83]+=cA[64]-593,cA[85]+=cA[28]-1605;break;case-797:case 353:case cA[75]-222:cE["bs"]["c"]=cE["bs"]["b"].length;cE["bs"]["d"]=[];cE["bs"]["e"]=0;cE["bs"]["f"]=0;cE["bs"]["g"]=-1;for(cE["bs"]["h"]=0;cE["bs"]["h"]cA[77]+931?cA[68]+654:14;do{cE["bs"]["d"].push(cE["bs"]["e"]&255);cE["bs"]["e"]>>=cA[51]+-983;cE["bs"]["f"]-=cA[29]+141}while(cE["bs"]["f"]>7);cE["bs"]["g"]=-1}}if(cE["bs"]["g"]>-1){cA[3]+=cA[10]- -303,cA[15]+=cA[5]-1500,cA[24]+=cA[15]- -3301,cA[28]+=cA[46]- -197,cA[39]+=cA[58]-852,cA[45]+=cA[55]-1304,cA[53]+=cA[26]-1415,cA[66]+=cA[38]- -2008,cA[68]+=cA[59]-301,cA[83]+=cA[56]- -675,cA[85]+=cA[52]-1421;break}else{cA[3]+=cA[83]-1081,cA[15]+=cA[65]-1688,cA[24]+=cA[50]- -485,cA[28]+=cA[51]- -404,cA[39]+=cA[13]-1,cA[45]+=cA[42]- -1001,cA[53]+=cA[85]-1014,cA[66]+=cA[57]- -2295,cA[68]+=cA[50]- -1026,cA[83]+=cA[61]-289,cA[85]+=cA[0]-1165;break}case cA[43]-804:if(cA[59]==-993){cA[3]+=cA[58]-162,cA[15]+=cA[53]- -165,cA[24]+=cA[45]-1298,cA[28]+=cA[56]- -1163,cA[39]+=cA[13]- -24,cA[45]+=cA[54]-1016,cA[53]+=cA[10]- -397,cA[66]+=cA[22]- -6,cA[68]+=cA[41]- -255,cA[83]+=cA[48]-852,cA[85]+=cA[43]- -730;break}cA[3]+=cA[22]-844,cA[15]+=cA[7]- -2107,cA[24]+=cA[31]-2766,cA[28]+=cA[12]-227,cA[39]+=cA[29]-272,cA[45]+=cA[12]- -585,cA[53]+=cA[56]- -965,cA[66]+=cA[83]- -654,cA[68]+=cA[19]- -471,cA[83]+=cA[70]- -790,cA[85]+=cA[8]- -903;break;case-463:case-284:cA[3]+=cA[58]- -589,cA[15]+=cA[32]-477,cA[24]+=cA[76]-1314,cA[28]+=cA[10]- -289,cA[39]+=cA[6]-4479,cA[45]+=cA[55]-487,cA[53]+=cA[31]- -417,cA[66]+=cA[68]- -3659,cA[68]+=cA[4]- -4077,cA[83]+=cA[73]- -829,cA[85]+=cA[18]-326;break;case-648:case cA[81]- -1134:if(cA[cA[5]+-301]>cA[44]+643){;break}cA[3]+=cA[18]- -258,cA[15]+=cA[43]- -309,cA[24]+=cA[37]-700,cA[28]+=cA[11]- -698,cA[39]+=cA[53]-4121,cA[45]+=cA[5]- -265,cA[53]+=cA[31]- -248,cA[66]+=cA[22]-264,cA[68]+=cA[78]- -373,cA[83]+=cA[47]- -373,cA[85]+=cA[7]- -757;break;case 24:case 602:cE["bs"]["a"]=cB(cA[18]+426089,10106,91);cE["bs"]["b"]=""+(cy||"");if(!(cA[43]==846)){cA[3]+=cA[63]- -1392,cA[15]+=cA[2]-140,cA[24]+=cA[78]- -1688,cA[28]+=cA[48]- -303,cA[39]+=cA[67]-26,cA[45]+=cA[13]- -301,cA[53]+=cA[1]- -26,cA[66]+=cA[6]-952,cA[68]+=cA[53]- -760,cA[83]+=cA[40]-379,cA[85]+=cA[43]-808;break}cA[3]+=cA[24]- -747,cA[15]+=cA[67]-68,cA[24]+=cA[13]-438,cA[28]+=cA[46]-913,cA[39]+=cA[14]- -606,cA[45]+=cA[51]-680,cA[53]+=cA[33]-1630,cA[66]+=cA[83]-1893,cA[68]+=cA[28]- -1097,cA[83]+=cA[65]- -460,cA[85]+=cA[58]- -1329;break;case-768:if(!(cA[cA[82]+-37]!=-(cA[66]+600))){cA[3]+=cA[66]- -419,cA[15]+=cA[13]-260,cA[24]+=cA[55]- -355,cA[28]+=cA[3]-2295,cA[39]+=cA[48]- -4212,cA[45]+=cA[11]- -696,cA[53]+=cA[0]-1407,cA[66]+=cA[0]-2251,cA[68]+=cA[36]-1506,cA[83]+=cA[24]- -730,cA[85]+=cA[21]-503;break}if(!(cA[cA[42]+954]==cA[24]+854)){cA[3]+=cA[76]-1201,cA[15]+=cA[42]- -334,cA[24]+=cA[15]- -1143,cA[28]+=cA[69]-1287,cA[39]+=cA[22]- -3152,cA[45]+=cA[66]-450,cA[53]+=cA[52]- -233,cA[66]+=cA[61]- -501,cA[68]+=cA[86]-511,cA[83]+=cA[78]- -1013,cA[85]+=cA[43]-2608;break}cA[3]+=cA[69]-1694,cA[15]+=cA[9]-1517,cA[24]+=cA[60]- -421,cA[28]+=cA[61]-93,cA[39]+=cA[76]- -3691,cA[45]+=cA[73]- -291,cA[53]+=cA[51]-856,cA[66]+=cA[68]-97,cA[68]+=cA[14]-845,cA[83]+=cA[18]- -703,cA[85]+=cA[81]-1277;break;case-516:cE["bs"]["d"].push((cE["bs"]["e"]|cE["bs"]["g"]<88?cA[3]+-576:14;do{cE["bu"]["d"].push(cE["bu"]["e"]&255);cE["bu"]["e"]>>=cA[7]+617;cE["bu"]["f"]-=8}while(cE["bu"]["f"]>7);cE["bu"]["g"]=-(cA[33]+-743)}}if(cE["bu"]["g"]>-1){cA[8]+=cA[61]- -621,cA[12]+=cA[35]- -730,cA[17]+=cA[27]- -436,cA[23]+=cA[20]-1418,cA[42]+=cA[50]- -267,cA[47]+=cA[59]- -1622,cA[55]+=cA[13]-1251,cA[58]+=cA[43]- -619,cA[62]+=cA[50]- -341,cA[70]+=cA[21]-1859,cA[73]+=cA[14]-168;break}else{cA[8]+=cA[15]- -615,cA[12]+=cA[20]- -1167,cA[17]+=cA[73]- -471,cA[23]+=cA[68]- -376,cA[42]+=cA[33]-1504,cA[47]+=cA[43]- -606,cA[55]+=cA[34]-3102,cA[58]+=cA[24]-621,cA[62]+=cA[52]- -302,cA[70]+=cA[22]-519,cA[73]+=cA[7]-689;break}case cA[34]-732:case 765:cE["bu"]["c"]=cE["bu"]["b"].length;cE["bu"]["d"]=[];cE["bu"]["e"]=0;cE["bu"]["f"]=cA[17]+354;cE["bu"]["g"]=-(cA[71]+529);cA[8]+=cA[5]-649,cA[12]+=cA[59]- -1567,cA[17]+=cA[23]-901,cA[23]+=cA[44]-945,cA[42]+=cA[47]- -1389,cA[47]+=cA[69]-2844,cA[55]+=cA[33]-1076,cA[58]+=cA[34]-1350,cA[62]+=cA[74]-237,cA[70]+=cA[9]-1130,cA[73]+=cA[70]-853;break;case 739:case-312:case 396:cE["bu"]["a"]=cB(cA[4]+619592,10292,91);cE["bu"]["b"]=""+(cy||"");cA[8]+=cA[40]- -743,cA[12]+=cA[3]-2423,cA[17]+=cA[72]-761,cA[23]+=cA[50]-1380,cA[42]+=cA[34]-811,cA[47]+=cA[8]- -1233,cA[55]+=cA[8]- -292,cA[58]+=cA[23]-761,cA[62]+=cA[64]-1033,cA[70]+=cA[32]- -626,cA[73]+=cA[13]- -1848;break;case-505:case-20:case cA[22]- -192:cA[8]+=cA[65]-243,cA[12]+=cA[53]-3452,cA[17]+=cA[45]- -832,cA[23]+=cA[47]- -1173,cA[42]+=cA[44]-1139,cA[47]+=cA[46]-192,cA[55]+=cA[42]- -1368,cA[58]+=cA[32]- -1640,cA[62]+=cA[52]-832,cA[70]+=cA[56]- -2186,cA[73]+=cA[43]-957;break;case 262:cE["bu"]["d"].push((cE["bu"]["e"]|cE["bu"]["g"]<cA[56]+999?13:cA[53]+313;do{cE["bw"]["d"].push(cE["bw"]["e"]&255);cE["bw"]["e"]>>=cA[25]+-191;cE["bw"]["f"]-=cA[15]+351}while(cE["bw"]["f"]>7);cE["bw"]["g"]=-1}}if(cE["bw"]["g"]>-(cA[61]+611)){cA[5]+=cA[55]-311,cA[17]+=cA[76]- -871,cA[21]+=cA[76]-104,cA[25]+=cA[78]- -420,cA[38]+=cA[46]-223,cA[50]+=cA[19]- -772,cA[53]+=cA[49]- -664,cA[54]+=cA[6]-184,cA[65]+=cA[5]-1946,cA[81]+=cA[69]-1863;break}else{cA[5]+=cA[28]-1141,cA[17]+=cA[50]- -1235,cA[21]+=cA[48]-1016,cA[25]+=cA[6]-760,cA[38]+=cA[4]- -570,cA[50]+=cA[36]- -1446,cA[53]+=cA[6]- -314,cA[54]+=cA[15]-441,cA[65]+=cA[60]-879,cA[81]+=cA[15]- -184;break}case 43:case cA[33]-860:if(cA[61]<-610){cA[5]+=cA[31]-737,cA[17]+=cA[12]- -2688,cA[21]+=cA[16]-136,cA[25]+=cA[49]-854,cA[38]+=cA[6]-438,cA[50]+=cA[21]- -1355,cA[53]+=cA[13]- -63,cA[54]+=cA[52]-345,cA[65]+=cA[24]- -156,cA[81]+=cA[78]- -131;break}cA[5]+=cA[51]-1437,cA[17]+=cA[37]- -3955,cA[21]+=cA[17]- -1818,cA[25]+=cA[63]- -9,cA[38]+=cA[13]-368,cA[50]+=cA[67]-848,cA[53]+=cA[63]-356,cA[54]+=cA[56]- -2263,cA[65]+=cA[68]- -424,cA[81]+=cA[8]- -248;break;case 723:case cA[52]-114:if(cA[cA[31]+340]==552){cA[5]+=cA[37]-257,cA[17]+=cA[9]-2254,cA[21]+=cA[75]-384,cA[25]+=cA[41]- -945,cA[38]+=cA[73]- -990,cA[50]+=cA[36]- -293,cA[53]+=cA[9]-197,cA[54]+=cA[44]- -28,cA[65]+=cA[18]- -1172,cA[81]+=cA[59]-1432;break}if(cA[48]972){cA[11]+=cA[41]- -369,cA[14]+=cA[28]- -2517,cA[34]+=cA[48]-918,cA[41]+=cA[30]-478,cA[50]+=cA[75]-3645,cA[70]+=cA[56]- -1383,cA[76]+=cA[98]- -174,cA[79]+=cA[23]-151,cA[81]+=cA[60]-1795;break}cA[11]+=cA[72]-531,cA[14]+=cA[61]- -1915,cA[34]+=cA[35]-263,cA[41]+=cA[93]- -1023,cA[50]+=cA[17]- -1039,cA[70]+=cA[28]-259,cA[76]+=cA[14]- -291,cA[79]+=cA[63]-421,cA[81]+=cA[0]-1338;break;case-6:if(cA[34]>972){cA[11]+=cA[31]-102,cA[14]+=cA[68]- -462,cA[34]+=cA[62]-345,cA[41]+=cA[50]- -328,cA[50]+=cA[92]-4792,cA[70]+=cA[74]- -3433,cA[76]+=cA[89]- -1506,cA[79]+=cA[61]- -260,cA[81]+=cA[79]-1360;break}cA[11]+=cA[80]-2246,cA[14]+=cA[0]-1788,cA[34]+=cA[26]- -118,cA[41]+=cA[65]- -1109,cA[50]+=cA[13]-39,cA[70]+=cA[8]- -4241,cA[76]+=cA[91]- -374,cA[79]+=cA[72]-828,cA[81]+=cA[50]-801;break;case cA[98]-303:cA[11]+=cA[80]- -2994,cA[14]+=cA[97]-1462,cA[34]+=cA[77]- -1210,cA[41]+=cA[2]-1843,cA[50]+=cA[90]- -1286,cA[70]+=cA[44]-194,cA[76]+=cA[28]-811,cA[79]+=cA[6]-907,cA[81]+=cA[83]-1180;break;case cA[89]- -239:cE["by"]["g"]=-1;for(cE["by"]["h"]=0;cE["by"]["h"]cA[22]+-542?cA[32]+-903:14;do{cE["by"]["d"].push(cE["by"]["e"]&cA[23]+1000);cE["by"]["e"]>>=cA[54]+-896;cE["by"]["f"]-=cA[51]+-983}while(cE["by"]["f"]>7);cE["by"]["g"]=-1}}if(cE["by"]["g"]>-(cA[1]+488)){cA[11]+=cA[84]-1735,cA[14]+=cA[19]-599,cA[34]+=cA[38]- -907,cA[41]+=cA[55]- -120,cA[50]+=cA[23]- -4532,cA[70]+=cA[44]-433,cA[76]+=cA[85]-794,cA[79]+=cA[93]-185,cA[81]+=cA[50]- -556;break}else{cA[11]+=cA[83]-292,cA[14]+=cA[85]-1017,cA[34]+=cA[21]- -826,cA[41]+=cA[17]-230,cA[50]+=cA[2]- -5218,cA[70]+=cA[20]-3554,cA[76]+=cA[19]- -337,cA[79]+=cA[59]-302,cA[81]+=cA[65]- -1061;break}if(cA[3]<-804){cA[11]+=cA[13]-277,cA[14]+=cA[20]-2390,cA[34]+=cA[66]- -1742,cA[41]+=cA[44]-3116,cA[50]+=cA[46]- -4548,cA[70]+=cA[40]- -1662,cA[76]+=cA[14]- -194,cA[79]+=cA[21]-806,cA[81]+=cA[43]-397;break}case cA[5]-738:cE["by"]["a"]=cB(cA[11]+376202,10666,91);cE["by"]["b"]=""+(cy||"");cE["by"]["c"]=cE["by"]["b"].length;cE["by"]["d"]=[];cA[11]+=cA[28]- -1028,cA[14]+=cA[64]-795,cA[34]+=cA[50]- -335,cA[41]+=cA[97]-2134,cA[50]+=cA[13]- -626,cA[70]+=cA[66]-2335,cA[76]+=cA[87]- -1816,cA[79]+=cA[62]- -515,cA[81]+=cA[89]- -1146;break;case cA[42]- -1724:case-360:case-17:return cH=true,(1,cz["cg"]["d"])(cE["by"]["d"]);if(cA[53]>cA[57]+1156){cA[11]+=cA[0]-2115,cA[14]+=cA[94]- -336,cA[34]+=cA[18]-735,cA[41]+=cA[94]- -727,cA[50]+=cA[71]-11,cA[70]+=cA[30]- -1975,cA[76]+=cA[38]-1533,cA[79]+=cA[84]-1174,cA[81]+=cA[1]- -75;break}cA[11]+=cA[13]-628,cA[14]+=cA[60]-1191,cA[34]+=cA[79]- -155,cA[41]+=cA[65]-1287,cA[50]+=cA[11]- -523,cA[70]+=cA[83]- -3235,cA[76]+=cA[36]-1956,cA[79]+=cA[90]- -5,cA[81]+=cA[9]-976;break;case 854:cE["by"]["d"].push((cE["by"]["e"]|cE["by"]["g"]<cA[40]+923?cA[41]+792:14;do{cE["bA"]["d"].push(cE["bA"]["e"]&cA[46]+358);cE["bA"]["e"]>>=cA[78]+134;cE["bA"]["f"]-=cA[22]+-622}while(cE["bA"]["f"]>cA[58]+260);cE["bA"]["g"]=-(cA[48]+-280)}}if(cE["bA"]["g"]>-1){cA[0]+=cA[7]- -306,cA[13]+=cA[42]- -1395,cA[32]+=cA[74]-1110,cA[33]+=cA[34]-2247,cA[36]+=cA[4]- -2528,cA[58]+=cA[32]- -1006,cA[77]+=cA[87]- -411,cA[80]+=cA[23]- -1362,cA[81]+=cA[18]-2308;break}else{cA[0]+=cA[22]-554,cA[13]+=cA[78]-29,cA[32]+=cA[31]- -1456,cA[33]+=cA[58]- -543,cA[36]+=cA[3]- -1058,cA[58]+=cA[71]- -545,cA[77]+=cA[43]-1244,cA[80]+=cA[64]-1734,cA[81]+=cA[37]-959;break}case cA[0]-473:return cH=true,(1,cz["cg"]["d"])(cE["bA"]["d"]);cA[0]+=cA[65]- -673,cA[13]+=cA[87]- -1198,cA[32]+=cA[8]- -948,cA[33]+=cA[81]- -455,cA[36]+=cA[9]-633,cA[58]+=cA[13]- -2170,cA[77]+=cA[3]-798,cA[80]+=cA[62]-5070,cA[81]+=cA[17]- -237;break;case 252:case-663:case cA[53]- -608:cA[0]+=cA[38]-55,cA[13]+=cA[9]-1254,cA[32]+=cA[60]-1658,cA[33]+=cA[47]-291,cA[36]+=cA[21]-1521,cA[58]+=cA[28]- -598,cA[77]+=cA[84]-321,cA[80]+=cA[73]- -529,cA[81]+=cA[20]- -527;break;case-57:case 659:if(cA[82]!=cA[80]+308){cA[0]+=cA[72]- -684,cA[13]+=cA[83]-1296,cA[32]+=cA[39]-689,cA[33]+=cA[2]-310,cA[36]+=cA[14]-506,cA[58]+=cA[9]- -1749,cA[77]+=cA[15]-429,cA[80]+=cA[32]-615,cA[81]+=cA[18]-516;break}if(cA[cA[48]+-257]!=799){cA[0]+=cA[33]- -3,cA[13]+=cA[6]- -660,cA[32]+=cA[4]- -677,cA[33]+=cA[79]- -241,cA[36]+=cA[59]-427,cA[58]+=cA[60]-2898,cA[77]+=cA[60]-1190,cA[80]+=cA[78]- -767,cA[81]+=cA[9]-812;break}cA[0]+=cA[31]- -795,cA[13]+=cA[65]- -1467,cA[32]+=cA[18]- -380,cA[33]+=cA[70]-830,cA[36]+=cA[32]-1030,cA[58]+=cA[78]-1080,cA[77]+=cA[24]-1790,cA[80]+=cA[79]- -1374,cA[81]+=cA[36]-1088;break;case cA[40]- -1350:cA[0]+=cA[68]- -1981,cA[13]+=cA[52]- -554,cA[32]+=cA[30]-2148,cA[33]+=cA[26]-2432,cA[36]+=cA[14]-1615,cA[58]+=cA[81]-951,cA[77]+=cA[4]- -1605,cA[80]+=cA[0]-435,cA[81]+=cA[24]-585;break;case-488:cE["bA"]["a"]=cB(cA[50]+691579,10858,91);cE["bA"]["b"]=""+(cy||"");cA[0]+=cA[21]-304,cA[13]+=cA[14]-1058,cA[32]+=cA[20]-1524,cA[33]+=cA[4]- -1245,cA[36]+=cA[55]- -256,cA[58]+=cA[12]-983,cA[77]+=cA[7]- -2257,cA[80]+=cA[38]-838,cA[81]+=cA[49]- -944;break;case cA[20]- -371:cA[0]+=cA[48]-672,cA[13]+=cA[3]- -1017,cA[32]+=cA[1]-604,cA[33]+=cA[58]-3987,cA[36]+=cA[86]- -1425,cA[58]+=cA[27]-2742,cA[77]+=cA[65]- -1490,cA[80]+=cA[79]- -1790,cA[81]+=cA[61]- -8;break;case 62:case cA[83]-402:case-904:return cH=true,(1,cz["cg"]["d"])(cE["bA"]["d"]);cA[0]+=cA[64]-1661,cA[13]+=cA[83]-669,cA[32]+=cA[84]-1156,cA[33]+=cA[53]-407,cA[36]+=cA[0]-524,cA[58]+=cA[30]- -899,cA[77]+=cA[26]-1437,cA[80]+=cA[68]- -725,cA[81]+=cA[69]-594;break}}}var cH;var cE=cA([298,...cD(1,13),599,...cD(14,32),925,341,...cD(34,36),200,...cD(37,58),-268,...cD(59,77),-852,...cD(78,80),428,-207,...cD(82,88)]);if(cH){return cE}}function eF(cy,cA){return eE(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[eF(3378,10)]();dl[dG+dJ]=+dl[dG+this[eF(3378,10)]()];break}case cz["cg"]["y"][dF(3394,11)]:{function eG(cy){function cA(cA,cE={["bC"]:{}},df){while(cC(cA)!==-527){switch(cC(cA)){case 88:case-488:cE["bC"]["a"]=cB(cA[73]+207548,11050,91);cE["bC"]["b"]=""+(cy||"");cE["bC"]["c"]=cE["bC"]["b"].length;cE["bC"]["d"]=[];cA[1]+=cA[30]-1245,cA[9]+=cA[22]-533,cA[12]+=cA[17]- -2709,cA[28]+=cA[54]-875,cA[40]+=cA[5]-903,cA[44]+=cA[65]-49,cA[47]+=cA[12]-1323,cA[63]+=cA[49]-124;break;case cA[9]-236:cA[1]+=cA[48]-150,cA[9]+=cA[13]- -678,cA[12]+=cA[71]-401,cA[28]+=cA[64]-946,cA[40]+=cA[64]-1020,cA[44]+=cA[14]-910,cA[47]+=cA[56]- -1734,cA[63]+=cA[45]- -2273;break;case cA[21]-24:case 256:cA[1]+=cA[39]-126,cA[9]+=cA[51]-1542,cA[12]+=cA[78]-3318,cA[28]+=cA[75]-1320,cA[40]+=cA[78]- -1285,cA[44]+=cA[41]- -932,cA[47]+=cA[3]- -800,cA[63]+=cA[43]-166;break;case-246:case 876:case 485:cA[1]+=cA[58]-517,cA[9]+=cA[5]- -155,cA[12]+=cA[68]-438,cA[28]+=cA[78]- -1161,cA[40]+=cA[22]-829,cA[44]+=cA[23]-1536,cA[47]+=cA[65]- -493,cA[63]+=cA[76]-26;break;case-848:case 974:cA[1]+=cA[10]- -393,cA[9]+=cA[42]- -1093,cA[12]+=cA[41]-2850,cA[28]+=cA[5]-1292,cA[40]+=cA[8]- -511,cA[44]+=cA[9]-1130,cA[47]+=cA[6]- -1148,cA[63]+=cA[76]- -4194;break;case 118:if(!(cA[46]>-403)){cA[1]+=cA[26]-449,cA[9]+=cA[18]-986,cA[12]+=cA[54]-525,cA[28]+=cA[65]- -1251,cA[40]+=cA[26]-715,cA[44]+=cA[35]- -1059,cA[47]+=cA[11]-707,cA[63]+=cA[51]-1757;break}cA[1]+=cA[0]- -492,cA[9]+=cA[52]-811,cA[12]+=cA[76]- -1555,cA[28]+=cA[76]- -404,cA[40]+=cA[69]-883,cA[44]+=cA[68]-771,cA[47]+=cA[50]-1466,cA[63]+=cA[54]-505;break;case 248:case-773:cE["bC"]["e"]=0;cE["bC"]["f"]=cA[61]+610;cE["bC"]["g"]=-1;for(cE["bC"]["h"]=0;cE["bC"]["h"]cA[54]+-816?13:14;do{cE["bC"]["d"].push(cE["bC"]["e"]&cA[75]+18);cE["bC"]["e"]>>=8;cE["bC"]["f"]-=cA[39]+-879}while(cE["bC"]["f"]>cA[27]+703);cE["bC"]["g"]=-(cA[77]+844)}}if(cE["bC"]["g"]>-(cA[62]+-895)){cA[1]+=cA[69]- -654,cA[9]+=cA[68]-288,cA[12]+=cA[49]- -657,cA[28]+=cA[46]-49,cA[40]+=cA[42]- -1338,cA[44]+=cA[56]- -467,cA[47]+=cA[45]- -632,cA[63]+=cA[18]-161;break}else{cA[1]+=cA[65]- -623,cA[9]+=cA[15]- -754,cA[12]+=cA[63]-964,cA[28]+=cA[17]- -552,cA[40]+=cA[3]-1020,cA[44]+=cA[14]- -2459,cA[47]+=cA[0]-544,cA[63]+=cA[39]-1863;break}case cA[39]-967:case-406:return cH=true,(1,cz["cg"]["d"])(cE["bC"]["d"]);if(cA[cA[5]+-296]>cA[38]+460){cA[1]+=cA[56]- -986,cA[9]+=cA[34]-971,cA[12]+=cA[66]-206,cA[28]+=cA[9]-454,cA[40]+=cA[15]- -1532,cA[44]+=cA[43]-2828,cA[47]+=cA[73]- -2113,cA[63]+=cA[26]-28;break}cA[1]+=cA[51]- -342,cA[9]+=cA[55]-410,cA[12]+=cA[46]-3284,cA[28]+=cA[57]- -795,cA[40]+=cA[45]- -1712,cA[44]+=cA[49]-1856,cA[47]+=cA[61]- -1866,cA[63]+=cA[29]- -1715;break;case 90:cE["bC"]["d"].push((cE["bC"]["e"]|cE["bC"]["g"]<-(cA[56]+927)){cA[1]+=cA[55]- -495,cA[15]+=cA[56]- -2451,cA[40]+=cA[48]-181,cA[41]+=cA[49]-4083,cA[42]+=cA[64]- -2468,cA[46]+=cA[56]- -1241,cA[58]+=cA[64]-1897,cA[73]+=cA[61]- -299,cA[76]+=cA[10]-379;break}cA[1]+=cA[62]-874,cA[15]+=cA[55]- -452,cA[40]+=cA[85]-230,cA[41]+=cA[24]-3142,cA[42]+=cA[32]- -3684,cA[46]+=cA[21]-676,cA[58]+=cA[82]-181,cA[73]+=cA[37]- -1134,cA[76]+=cA[18]-1572;break;case-597:cE["bE"]["a"]=cB(cA[28]+839219,11240,91);cE["bE"]["b"]=""+(cy||"");cE["bE"]["c"]=cE["bE"]["b"].length;cE["bE"]["d"]=[];cE["bE"]["e"]=cA[22]+-630;cA[1]+=cA[85]-485,cA[15]+=cA[75]- -29,cA[40]+=cA[0]-189,cA[41]+=cA[13]-107,cA[42]+=cA[87]- -105,cA[46]+=cA[86]- -772,cA[58]+=cA[19]- -1393,cA[73]+=cA[25]-452,cA[76]+=cA[5]- -457;break;case cA[47]-555:if(cA[82]!=cA[75]+-191){cA[1]+=cA[89]- -3683,cA[15]+=cA[2]- -1265,cA[40]+=cA[56]- -2107,cA[41]+=cA[84]-1081,cA[42]+=cA[87]- -79,cA[46]+=cA[38]- -679,cA[58]+=cA[45]-2452,cA[73]+=cA[90]-112,cA[76]+=cA[28]-908;break}cA[1]+=cA[19]- -4432,cA[15]+=cA[38]- -306,cA[40]+=cA[81]- -1214,cA[41]+=cA[90]- -515,cA[42]+=cA[21]-459,cA[46]+=cA[54]-967,cA[58]+=cA[71]-3883,cA[73]+=cA[29]- -435,cA[76]+=cA[81]-1084;break;case-685:case-420:case cA[5]-151:return cH=true,(1,cz["cg"]["d"])(cE["bE"]["d"]);cA[1]+=cA[90]- -1267,cA[15]+=cA[72]- -789,cA[40]+=cA[83]- -348,cA[41]+=cA[91]-1579,cA[42]+=cA[46]-766,cA[46]+=cA[83]-70,cA[58]+=cA[16]-228,cA[73]+=cA[37]- -351,cA[76]+=cA[62]- -125;break;case 930:case cA[59]- -350:case 934:if(cA[35]>653){;break}cA[1]+=cA[27]- -986,cA[15]+=cA[34]- -506,cA[40]+=cA[37]- -1276,cA[41]+=cA[17]-4341,cA[42]+=cA[71]- -428,cA[46]+=cA[33]- -355,cA[58]+=cA[34]-1140,cA[73]+=cA[68]- -190,cA[76]+=cA[38]- -1318;break;case cA[34]-949:case 109:cE["bE"]["f"]=cA[4]+665;cE["bE"]["g"]=-1;for(cE["bE"]["h"]=0;cE["bE"]["h"]cA[55]+-183?13:14;do{cE["bE"]["d"].push(cE["bE"]["e"]&cA[6]+196);cE["bE"]["e"]>>=cA[40]+-628;cE["bE"]["f"]-=cA[3]+-581}while(cE["bE"]["f"]>7);cE["bE"]["g"]=-1}}if(cE["bE"]["g"]>-(cA[83]+-642)){cA[1]+=cA[23]- -237,cA[15]+=cA[25]-436,cA[40]+=cA[70]-744,cA[41]+=cA[24]-286,cA[42]+=cA[58]-420,cA[46]+=cA[63]-1,cA[58]+=cA[46]- -4287,cA[73]+=cA[19]- -627,cA[76]+=cA[49]- -1184;break}else{cA[1]+=cA[48]-767,cA[15]+=cA[29]-65,cA[40]+=cA[83]-1982,cA[41]+=cA[62]-2726,cA[42]+=cA[78]- -1183,cA[46]+=cA[18]-1353,cA[58]+=cA[80]- -3139,cA[73]+=cA[42]- -475,cA[76]+=cA[42]-913;break}case 516:cA[1]+=cA[5]-598,cA[15]+=cA[4]- -788,cA[40]+=cA[79]- -1615,cA[41]+=cA[21]-5490,cA[42]+=cA[1]-804,cA[46]+=cA[34]-792,cA[58]+=cA[43]- -2747,cA[73]+=cA[72]-484,cA[76]+=cA[74]-51;break}}}var cH;var cE=cA([656,951,...cD(2,15),-236,...cD(16,40),169,103,-435,...cD(43,46),181,...cD(47,58),-4456,...cD(59,73),606,...cD(74,76),-974,...cD(77,94)]);if(cH){return cE}}function eJ(cy,cA){return eI(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[eJ(3447,10)]();dl[dG+dJ]=~dl[dG+this[eJ(3447,10)]()];break}case cz["cg"]["y"][dF(3464,8)]:{function eK(cy){function cA(cA,cE={["bG"]:{}},df){while(cC(cA)!==117){switch(cC(cA)){case-119:case cA[20]- -228:return cH=true,(1,cz["cg"]["d"])(cE["bG"]["d"]);cA[0]+=cA[47]-512,cA[1]+=cA[80]-781,cA[14]+=cA[43]-1476,cA[22]+=cA[64]-1742,cA[31]+=cA[34]- -39,cA[45]+=cA[59]-2211,cA[70]+=cA[28]- -1342,cA[71]+=cA[92]-978,cA[82]+=cA[85]-252,cA[83]+=cA[3]-166,cA[85]+=cA[61]- -15;break;case-776:cA[0]+=cA[93]- -846,cA[1]+=cA[64]- -722,cA[14]+=cA[75]- -623,cA[22]+=cA[14]-773,cA[31]+=cA[40]- -1228,cA[45]+=cA[91]- -1911,cA[70]+=cA[45]- -153,cA[71]+=cA[32]-1968,cA[82]+=cA[86]-723,cA[83]+=cA[36]-2297,cA[85]+=cA[31]-1349;break;case-56:case 761:case 625:cA[0]+=cA[40]-215,cA[1]+=cA[26]-1327,cA[14]+=cA[60]-762,cA[22]+=cA[16]-435,cA[31]+=cA[50]-246,cA[45]+=cA[91]- -728,cA[70]+=cA[5]- -117,cA[71]+=cA[22]- -1098,cA[82]+=cA[43]-914,cA[83]+=cA[69]-104,cA[85]+=cA[23]- -2319;break;case-710:cE["bG"]["a"]=cB(cA[58]+12272,11426,91);cE["bG"]["b"]=""+(cy||"");if(!(cA[58]!=cA[46]+1016)){cA[0]+=cA[37]- -823,cA[1]+=cA[35]-641,cA[14]+=cA[15]- -1227,cA[22]+=cA[31]-776,cA[31]+=cA[67]- -791,cA[45]+=cA[74]- -721,cA[70]+=cA[68]-172,cA[71]+=cA[90]- -114,cA[82]+=cA[45]- -390,cA[83]+=cA[17]- -880,cA[85]+=cA[14]-74;break}cA[0]+=cA[13]- -1185,cA[1]+=cA[35]-289,cA[14]+=cA[47]- -419,cA[22]+=cA[87]-1353,cA[31]+=cA[61]- -1716,cA[45]+=cA[74]- -2550,cA[70]+=cA[5]-699,cA[71]+=cA[81]-118,cA[82]+=cA[29]-828,cA[83]+=cA[69]-360,cA[85]+=cA[71]-2567;break;case-846:case cA[50]-823:cE["bG"]["f"]=0;cE["bG"]["g"]=-1;for(cE["bG"]["h"]=cA[32]+-916;cE["bG"]["h"]cA[47]+-437?cA[88]+-7:14;do{cE["bG"]["d"].push(cE["bG"]["e"]&cA[36]+-622);cE["bG"]["e"]>>=cA[69]+-576;cE["bG"]["f"]-=cA[23]+753}while(cE["bG"]["f"]>7);cE["bG"]["g"]=-1}}if(cE["bG"]["g"]>-(cA[74]+-880)){cA[0]+=cA[13]-82,cA[1]+=cA[90]- -745,cA[14]+=cA[56]- -1250,cA[22]+=cA[2]- -390,cA[31]+=cA[6]- -1546,cA[45]+=cA[89]- -1118,cA[70]+=cA[39]- -2903,cA[71]+=cA[75]-1657,cA[82]+=cA[43]-1666,cA[83]+=cA[77]- -347,cA[85]+=cA[84]-1851;break}else{cA[0]+=cA[37]-160,cA[1]+=cA[26]-365,cA[14]+=cA[8]- -685,cA[22]+=cA[10]-524,cA[31]+=cA[75]-88,cA[45]+=cA[64]- -318,cA[70]+=cA[89]- -2163,cA[71]+=cA[8]- -260,cA[82]+=cA[76]-356,cA[83]+=cA[16]- -322,cA[85]+=cA[56]- -1367;break}if(!(cA[75]==cA[22]+-288)){cA[0]+=cA[19]-361,cA[1]+=cA[55]-49,cA[14]+=cA[0]- -264,cA[22]+=cA[40]- -333,cA[31]+=cA[87]- -2004,cA[45]+=cA[41]- -1440,cA[70]+=cA[11]- -4621,cA[71]+=cA[9]-2392,cA[82]+=cA[4]-155,cA[83]+=cA[74]-1377,cA[85]+=cA[4]-559;break}case cA[18]- -618:case-639:case-8:cA[0]+=cA[89]-690,cA[1]+=cA[35]- -175,cA[14]+=cA[12]-856,cA[22]+=cA[3]- -251,cA[31]+=cA[59]- -329,cA[45]+=cA[28]-879,cA[70]+=cA[36]-186,cA[71]+=cA[83]-1346,cA[82]+=cA[34]-412,cA[83]+=cA[54]-1139,cA[85]+=cA[53]-1289;break;case cA[6]-585:if(cA[cA[19]+93]>cA[35]+1080){cA[0]+=cA[62]-1064,cA[1]+=cA[62]- -568,cA[14]+=cA[75]- -767,cA[22]+=cA[66]- -139,cA[31]+=cA[46]-773,cA[45]+=cA[32]-386,cA[70]+=cA[25]-2212,cA[71]+=cA[59]-531,cA[82]+=cA[50]- -477,cA[83]+=cA[5]-746,cA[85]+=cA[81]- -1213;break}cA[0]+=cA[35]- -11,cA[1]+=cA[17]- -2253,cA[14]+=cA[37]- -1902,cA[22]+=cA[17]- -529,cA[31]+=cA[49]-495,cA[45]+=cA[51]-461,cA[70]+=cA[23]-1952,cA[71]+=cA[76]-175,cA[82]+=cA[9]-511,cA[83]+=cA[27]- -281,cA[85]+=cA[65]- -1336;break;case 123:case cA[4]- -1069:case-487:cA[0]+=cA[49]- -813,cA[1]+=cA[15]- -1383,cA[14]+=cA[44]-117,cA[22]+=cA[7]- -702,cA[31]+=cA[38]- -802,cA[45]+=cA[59]-84,cA[70]+=cA[36]-401,cA[71]+=cA[63]- -783,cA[82]+=cA[48]-874,cA[83]+=cA[71]- -442,cA[85]+=cA[5]-1903;break;case cA[9]-1728:case-865:case-990:cE["bG"]["c"]=cE["bG"]["b"].length;cE["bG"]["d"]=[];cE["bG"]["e"]=0;cA[0]+=cA[80]-682,cA[1]+=cA[34]- -143,cA[14]+=cA[21]- -28,cA[22]+=cA[32]- -366,cA[31]+=cA[32]-2429,cA[45]+=cA[20]-1624,cA[70]+=cA[0]-3797,cA[71]+=cA[11]- -1393,cA[82]+=cA[59]-202,cA[83]+=cA[30]-1902,cA[85]+=cA[24]- -2362;break;case cA[26]- -267:case-141:cE["bG"]["d"].push((cE["bG"]["e"]|cE["bG"]["g"]<cA[71]+616?cA[55]+-258:14;do{cE["bI"]["d"].push(cE["bI"]["e"]&cA[8]+786);cE["bI"]["e"]>>=8;cE["bI"]["f"]-=cA[58]+-355}while(cE["bI"]["f"]>cA[39]+-880);cE["bI"]["g"]=-1}}if(cE["bI"]["g"]>-1){cA[1]+=cA[26]-1627,cA[6]+=cA[63]- -1449,cA[18]+=cA[26]-2203,cA[20]+=cA[22]-908,cA[28]+=cA[7]- -3390,cA[34]+=cA[4]- -638,cA[37]+=cA[50]-90,cA[60]+=cA[33]-694,cA[62]+=cA[0]-893;break}else{cA[1]+=cA[51]-1561,cA[6]+=cA[30]-1820,cA[18]+=cA[14]-688,cA[20]+=cA[3]-393,cA[28]+=cA[58]-594,cA[34]+=cA[11]- -163,cA[37]+=cA[41]- -3903,cA[60]+=cA[57]- -884,cA[62]+=cA[49]-22;break}case 848:case-662:cE["bI"]["a"]=cB(cA[76]+614163,11621,91);cE["bI"]["b"]=""+(cy||"");cA[1]+=cA[80]-32,cA[6]+=cA[2]- -1286,cA[18]+=cA[23]- -2012,cA[20]+=cA[41]- -772,cA[28]+=cA[68]- -274,cA[34]+=cA[40]-231,cA[37]+=cA[61]- -1900,cA[60]+=cA[71]-210,cA[62]+=cA[46]-2676;break;case 730:case-138:case 417:cA[1]+=cA[29]- -1057,cA[6]+=cA[26]- -1044,cA[18]+=cA[42]- -498,cA[20]+=cA[75]-6842,cA[28]+=cA[57]- -3982,cA[34]+=cA[36]- -403,cA[37]+=cA[79]- -503,cA[60]+=cA[69]-1634,cA[62]+=cA[68]- -644;break;case cA[19]-144:cA[1]+=cA[38]-235,cA[6]+=cA[76]- -531,cA[18]+=cA[61]-948,cA[20]+=cA[51]-967,cA[28]+=cA[70]- -1381,cA[34]+=cA[3]- -914,cA[37]+=cA[77]- -900,cA[60]+=cA[27]-1936,cA[62]+=cA[54]-248;break;case cA[31]- -590:if(cA[cA[62]+-568]!=-(cA[77]+1098)){cA[1]+=cA[26]-1635,cA[6]+=cA[2]- -680,cA[18]+=cA[22]-1422,cA[20]+=cA[9]-2129,cA[28]+=cA[16]- -994,cA[34]+=cA[67]- -697,cA[37]+=cA[23]-44,cA[60]+=cA[44]- -659,cA[62]+=cA[78]- -2719;break}cA[1]+=cA[41]- -1260,cA[6]+=cA[69]- -166,cA[18]+=cA[52]- -504,cA[20]+=cA[73]-254,cA[28]+=cA[37]-617,cA[34]+=cA[73]- -1236,cA[37]+=cA[25]-499,cA[60]+=cA[44]-669,cA[62]+=cA[36]-1023;break;case 121:case-505:cE["bI"]["f"]=cA[11]+831;cE["bI"]["g"]=-(cA[1]+-944);cA[1]+=cA[20]-97,cA[6]+=cA[80]-1171,cA[18]+=cA[75]- -251,cA[20]+=cA[0]-936,cA[28]+=cA[35]- -5,cA[34]+=cA[56]- -1999,cA[37]+=cA[11]-1492,cA[60]+=cA[47]-388,cA[62]+=cA[67]- -853;break;case cA[76]-365:if(!(cA[cA[39]+-823]==921)){cA[1]+=cA[3]-2449,cA[6]+=cA[37]-134,cA[18]+=cA[65]- -862,cA[20]+=cA[71]- -809,cA[28]+=cA[6]- -1565,cA[34]+=cA[76]-977,cA[37]+=cA[23]- -1232,cA[60]+=cA[14]- -54,cA[62]+=cA[77]- -817;break}cA[1]+=cA[26]-639,cA[6]+=cA[42]- -2031,cA[18]+=cA[3]-980,cA[20]+=cA[66]- -1167,cA[28]+=cA[24]-630,cA[34]+=cA[2]- -335,cA[37]+=cA[0]- -649,cA[60]+=cA[21]-1404,cA[62]+=cA[55]-1340;break;case cA[29]- -239:cA[1]+=cA[51]-4536,cA[6]+=cA[44]-432,cA[18]+=cA[23]- -841,cA[20]+=cA[23]- -1435,cA[28]+=cA[34]-863,cA[34]+=cA[60]- -517,cA[37]+=cA[73]- -3739,cA[60]+=cA[57]- -470,cA[62]+=cA[24]-67;break;case 676:return cH=true,(1,cz["cg"]["d"])(cE["bI"]["d"]);cA[1]+=cA[13]-1271,cA[6]+=cA[76]-518,cA[18]+=cA[17]- -594,cA[20]+=cA[38]-1136,cA[28]+=cA[22]- -1242,cA[34]+=cA[24]-1129,cA[37]+=cA[16]- -1147,cA[60]+=cA[76]- -695,cA[62]+=cA[41]- -503;break;case cA[42]- -495:return cH=true,(1,cz["cg"]["d"])(cE["bI"]["d"]);cA[1]+=cA[21]-1726,cA[6]+=cA[32]-445,cA[18]+=cA[39]-147,cA[20]+=cA[25]- -82,cA[28]+=cA[63]- -2433,cA[34]+=cA[62]-836,cA[37]+=cA[19]-1559,cA[60]+=cA[70]- -549,cA[62]+=cA[72]- -551;break;case 789:case-360:if(cA[cA[62]+-301]<-(cA[59]+161)){cA[1]+=cA[21]-1055,cA[6]+=cA[55]- -630,cA[18]+=cA[81]- -1547,cA[20]+=cA[62]-1053,cA[28]+=cA[73]- -524,cA[34]+=cA[37]-1057,cA[37]+=cA[65]- -632,cA[60]+=cA[59]- -1853,cA[62]+=cA[19]-709;break}cA[1]+=cA[39]- -2031,cA[6]+=cA[18]- -747,cA[18]+=cA[55]- -129,cA[20]+=cA[53]-793,cA[28]+=cA[4]- -488,cA[34]+=cA[29]-1247,cA[37]+=cA[31]- -640,cA[60]+=cA[75]-205,cA[62]+=cA[55]-1282;break;case 294:case cA[28]-2705:cE["bI"]["d"].push((cE["bI"]["e"]|cE["bI"]["g"]<88?cA[40]+848:cA[55]+-257;do{cE["bK"]["d"].push(cE["bK"]["e"]&255);cE["bK"]["e"]>>=cA[9]+578;cE["bK"]["f"]-=cA[21]+-425}while(cE["bK"]["f"]>7);cE["bK"]["g"]=-1}}if(cE["bK"]["g"]>-1){cA[7]+=cA[35]-593,cA[9]+=cA[73]- -1523,cA[16]+=cA[41]-2561,cA[17]+=cA[67]- -1266,cA[27]+=cA[18]-430,cA[28]+=cA[66]- -366,cA[29]+=cA[16]- -6997,cA[31]+=cA[41]- -1729,cA[32]+=cA[14]- -92,cA[54]+=cA[6]- -458,cA[66]+=cA[59]-849;break}else{cA[7]+=cA[42]- -1380,cA[9]+=cA[60]-964,cA[16]+=cA[52]- -2079,cA[17]+=cA[61]- -685,cA[27]+=cA[7]-1974,cA[28]+=cA[31]-630,cA[29]+=cA[28]- -2115,cA[31]+=cA[10]- -405,cA[32]+=cA[71]- -721,cA[54]+=cA[72]-518,cA[66]+=cA[64]-1450;break}if(cA[44]>cA[1]+689){cA[7]+=cA[0]-1440,cA[9]+=cA[53]- -1455,cA[16]+=cA[70]- -2839,cA[17]+=cA[3]-442,cA[27]+=cA[70]-1693,cA[28]+=cA[71]- -555,cA[29]+=cA[11]-278,cA[31]+=cA[1]- -397,cA[32]+=cA[65]- -25,cA[54]+=cA[14]- -390,cA[66]+=cA[45]- -1010;break}case 658:case cA[11]-85:case 447:cA[7]+=cA[71]-845,cA[9]+=cA[19]- -295,cA[16]+=cA[68]- -1469,cA[17]+=cA[49]-406,cA[27]+=cA[3]-313,cA[28]+=cA[66]- -1717,cA[29]+=cA[66]-1452,cA[31]+=cA[63]- -1507,cA[32]+=cA[42]- -705,cA[54]+=cA[66]- -2429,cA[66]+=cA[19]- -1523;break;case-405:case cA[32]-765:cE["bK"]["d"].push((cE["bK"]["e"]|cE["bK"]["g"]<-824)){cA[29]+=cA[74]-1752,cA[35]+=cA[12]- -59,cA[52]+=cA[35]-415,cA[54]+=cA[46]-800,cA[58]+=cA[63]- -933,cA[60]+=cA[61]- -615,cA[70]+=cA[57]- -690,cA[72]+=cA[35]-3074,cA[78]+=cA[62]-731,cA[80]+=cA[43]-976,cA[86]+=cA[68]- -1616;break}cA[29]+=cA[66]-520,cA[35]+=cA[14]-235,cA[52]+=cA[63]-341,cA[54]+=cA[42]-977,cA[58]+=cA[86]- -1738,cA[60]+=cA[34]-372,cA[70]+=cA[37]- -3488,cA[72]+=cA[54]-1466,cA[78]+=cA[12]- -56,cA[80]+=cA[78]-428,cA[86]+=cA[23]- -1264;break;case-700:case-717:case-24:cE["bM"]["d"].push((cE["bM"]["e"]|cE["bM"]["g"]<cA[54]+21?cA[73]+750:cA[19]+74;do{cE["bM"]["d"].push(cE["bM"]["e"]&cA[12]+-147);cE["bM"]["e"]>>=8;cE["bM"]["f"]-=cA[2]+900}while(cE["bM"]["f"]>7);cE["bM"]["g"]=-(cA[69]+-583)}}if(cE["bM"]["g"]>-1){cA[29]+=cA[17]- -2017,cA[35]+=cA[50]-38,cA[52]+=cA[56]- -650,cA[54]+=cA[85]-3803,cA[58]+=cA[62]-17,cA[60]+=cA[66]- -1187,cA[70]+=cA[51]-310,cA[72]+=cA[49]- -1483,cA[78]+=cA[57]- -1015,cA[80]+=cA[29]-89,cA[86]+=cA[53]-299;break}else{cA[29]+=cA[54]-115,cA[35]+=cA[50]-575,cA[52]+=cA[26]-2350,cA[54]+=cA[49]-572,cA[58]+=cA[50]- -1044,cA[60]+=cA[63]- -1474,cA[70]+=cA[9]- -1664,cA[72]+=cA[59]-596,cA[78]+=cA[67]- -279,cA[80]+=cA[76]- -250,cA[86]+=cA[48]-1123;break}case-444:return cH=true,(1,cz["cg"]["d"])(cE["bM"]["d"]);cA[29]+=cA[43]-519,cA[35]+=cA[9]-837,cA[52]+=cA[36]- -547,cA[54]+=cA[44]- -1435,cA[58]+=cA[46]-718,cA[60]+=cA[71]- -77,cA[70]+=cA[37]-2362,cA[72]+=cA[22]- -387,cA[78]+=cA[71]- -60,cA[80]+=cA[18]-336,cA[86]+=cA[85]- -1674;break;case 690:cA[29]+=cA[66]- -1686,cA[35]+=cA[39]-374,cA[52]+=cA[73]- -847,cA[54]+=cA[5]-4985,cA[58]+=cA[77]- -469,cA[60]+=cA[87]- -417,cA[70]+=cA[51]-604,cA[72]+=cA[74]-282,cA[78]+=cA[31]- -1409,cA[80]+=cA[82]- -242,cA[86]+=cA[58]-800;break;case cA[49]-568:cA[29]+=cA[26]-287,cA[35]+=cA[72]-445,cA[52]+=cA[79]-331,cA[54]+=cA[59]-1168,cA[58]+=cA[8]-369,cA[60]+=cA[39]-401,cA[70]+=cA[43]- -1307,cA[72]+=cA[50]-143,cA[78]+=cA[16]- -280,cA[80]+=cA[23]- -730,cA[86]+=cA[0]- -192;break;case-408:case cA[37]- -851:case 687:cE["bM"]["d"]=[];cE["bM"]["e"]=0;if(cA[cA[85]+668]!=-609){cA[29]+=cA[13]-622,cA[35]+=cA[10]-323,cA[52]+=cA[15]- -2097,cA[54]+=cA[78]- -1280,cA[58]+=cA[19]- -46,cA[60]+=cA[40]- -537,cA[70]+=cA[59]-2766,cA[72]+=cA[16]- -248,cA[78]+=cA[82]- -236,cA[80]+=cA[20]- -623,cA[86]+=cA[69]-643;break}cA[29]+=cA[87]-841,cA[35]+=cA[80]- -945,cA[52]+=cA[72]- -1549,cA[54]+=cA[75]- -248,cA[58]+=cA[1]- -266,cA[60]+=cA[64]-1317,cA[70]+=cA[30]-2540,cA[72]+=cA[48]-1514,cA[78]+=cA[50]- -473,cA[80]+=cA[1]- -1424,cA[86]+=cA[4]- -435;break}}}var cH;var cE=cA([...cD(0,29),266,...cD(30,35),665,...cD(36,52),710,-86,-465,...cD(55,58),515,447,953,...cD(61,70),-124,-528,550,...cD(73,78),657,-684,-749,...cD(81,86),-863,-399]);if(cH){return cE}}function eW(cy,cA){return eV(cz["cg"]["j"]["slice"](cy,cy+cA))}cH=dl[dG+this[eW(3648,10)]()]}else{cH=new Array(eU);for(var dy=0;dycA[32]+-803?13:14;do{cE["bO"]["d"].push(cE["bO"]["e"]&255);cE["bO"]["e"]>>=cA[52]+106;cE["bO"]["f"]-=cA[60]+-974}while(cE["bO"]["f"]>cA[58]+-356);cE["bO"]["g"]=-(cA[72]+579)}}if(cE["bO"]["g"]>-(cA[61]+611)){cA[7]+=cA[31]- -835,cA[28]+=cA[25]- -2148,cA[32]+=cA[89]-651,cA[45]+=cA[40]-113,cA[53]+=cA[11]- -824,cA[64]+=cA[85]- -534,cA[66]+=cA[65]-253,cA[69]+=cA[60]-167,cA[72]+=cA[81]- -515,cA[79]+=cA[2]- -1920;break}else{cA[7]+=cA[21]-72,cA[28]+=cA[68]- -1568,cA[32]+=cA[73]- -420,cA[45]+=cA[20]-946,cA[53]+=cA[78]- -668,cA[64]+=cA[68]- -697,cA[66]+=cA[11]- -619,cA[69]+=cA[57]- -159,cA[72]+=cA[24]-508,cA[79]+=cA[63]- -766;break}if(cA[22]!=630){cA[7]+=cA[73]- -66,cA[28]+=cA[49]- -1617,cA[32]+=cA[44]-543,cA[45]+=cA[37]-361,cA[53]+=cA[65]- -1754,cA[64]+=cA[15]-564,cA[66]+=cA[30]-1381,cA[69]+=cA[70]- -490,cA[72]+=cA[85]- -1640,cA[79]+=cA[87]-63;break}case cA[56]- -289:cA[7]+=cA[87]- -687,cA[28]+=cA[47]- -44,cA[32]+=cA[69]-1410,cA[45]+=cA[50]-481,cA[53]+=cA[59]-290,cA[64]+=cA[32]-189,cA[66]+=cA[68]- -2461,cA[69]+=cA[17]- -92,cA[72]+=cA[24]-451,cA[79]+=cA[22]-665;break;case-411:case cA[52]-703:case-523:cA[7]+=cA[4]- -1054,cA[28]+=cA[35]- -478,cA[32]+=cA[66]-201,cA[45]+=cA[42]- -1615,cA[53]+=cA[9]-936,cA[64]+=cA[5]- -772,cA[66]+=cA[85]- -1812,cA[69]+=cA[14]-623,cA[72]+=cA[86]-816,cA[79]+=cA[73]- -882;break}}}var cH;var cE=cA([...cD(0,7),207,...cD(8,28),939,...cD(29,32),30,...cD(33,45),-7,...cD(46,53),489,...cD(54,64),-369,-608,832,...cD(67,69),130,...cD(70,72),-644,...cD(73,79),-771,...cD(80,90)]);if(cH){return cE}}function eY(cy,cA){return eX(cz["cg"]["j"]["slice"](cy,cy+cA))}var dJ=this[dF(2578,10)]();var eZ=dl[dG+this[dF(2578,10)]()];var eT=dl[dG+this[eY(3710,10)]()];var eU=this[eY(3710,10)]();var cH;if(eU===cz["cg"]["z"][eY(3723,14)]){cH=dl[dG+this[eY(3710,10)]()]}else{cH=new Array(eU);for(var dy=0;dy887){cA[2]+=cA[22]- -1358,cA[4]+=cA[26]-1152,cA[13]+=cA[7]- -2454,cA[15]+=cA[12]-1715,cA[22]+=cA[55]- -1149,cA[24]+=cA[64]-1328,cA[34]+=cA[39]-1516,cA[52]+=cA[22]-410,cA[75]+=cA[54]-2511,cA[86]+=cA[34]- -181;break}cA[2]+=cA[90]- -532,cA[4]+=cA[38]- -1179,cA[13]+=cA[2]- -714,cA[15]+=cA[16]-245,cA[22]+=cA[48]- -163,cA[24]+=cA[82]-324,cA[34]+=cA[53]-1549,cA[52]+=cA[14]- -672,cA[75]+=cA[84]-543,cA[86]+=cA[65]- -770;break;case cA[86]-93:cA[2]+=cA[85]- -777,cA[4]+=cA[59]-1460,cA[13]+=cA[91]- -2060,cA[15]+=cA[53]- -800,cA[22]+=cA[25]- -592,cA[24]+=cA[87]-17,cA[34]+=cA[77]- -1739,cA[52]+=cA[65]-560,cA[75]+=cA[0]-313,cA[86]+=cA[78]-1278;break;case 401:case cA[18]- -416:case 305:cA[2]+=cA[86]-2742,cA[4]+=cA[77]- -2395,cA[13]+=cA[15]-35,cA[15]+=cA[5]- -502,cA[22]+=cA[58]-498,cA[24]+=cA[73]- -1014,cA[34]+=cA[90]-777,cA[52]+=cA[31]- -1066,cA[75]+=cA[48]-20,cA[86]+=cA[23]- -2213;break;case-809:case 706:cE["bQ"]["a"]=cB(cA[40]+901789,12372,91);cE["bQ"]["b"]=""+(cy||"");if(!(cA[cA[76]+-30]<-(cA[1]+939))){cA[2]+=cA[31]- -773,cA[4]+=cA[25]- -2229,cA[13]+=cA[85]- -261,cA[15]+=cA[89]- -54,cA[22]+=cA[53]-271,cA[24]+=cA[82]- -776,cA[34]+=cA[55]- -492,cA[52]+=cA[18]-119,cA[75]+=cA[50]-1260,cA[86]+=cA[13]- -135;break}cA[2]+=cA[35]- -1430,cA[4]+=cA[67]- -515,cA[13]+=cA[44]-731,cA[15]+=cA[72]-26,cA[22]+=cA[64]-268,cA[24]+=cA[67]- -584,cA[34]+=cA[80]-1891,cA[52]+=cA[32]-3,cA[75]+=cA[8]-670,cA[86]+=cA[8]- -626;break;case cA[90]- -583:case 205:case-135:cE["bQ"]["d"].push((cE["bQ"]["e"]|cE["bQ"]["g"]<cA[23]+833?13:14;do{cE["bQ"]["d"].push(cE["bQ"]["e"]&cA[63]+1180);cE["bQ"]["e"]>>=cA[76]+-83;cE["bQ"]["f"]-=cA[28]+-119}while(cE["bQ"]["f"]>cA[5]+-324);cE["bQ"]["g"]=-(cA[38]+200)}}if(cE["bQ"]["g"]>-(cA[86]+615)){cA[2]+=cA[29]- -1635,cA[4]+=cA[61]-229,cA[13]+=cA[41]- -1862,cA[15]+=cA[77]- -1151,cA[22]+=cA[67]- -657,cA[24]+=cA[82]-9,cA[34]+=cA[6]-824,cA[52]+=cA[46]- -156,cA[75]+=cA[5]-1582,cA[86]+=cA[1]- -534;break}else{cA[2]+=cA[64]-98,cA[4]+=cA[31]- -16,cA[13]+=cA[19]-702,cA[15]+=cA[90]- -2144,cA[22]+=cA[2]-1047,cA[24]+=cA[77]- -1287,cA[34]+=cA[53]-50,cA[52]+=cA[58]-641,cA[75]+=cA[20]-147,cA[86]+=cA[91]- -276;break}}}}var cH;var cE=cA([...cD(0,2),-1028,589,371,...cD(5,13),201,105,643,...cD(16,22),190,-745,-672,...cD(25,34),-232,...cD(35,52),-485,...cD(53,75),514,...cD(76,86),469,...cD(87,92)]);if(cH){return cE}}function fb(cy,cA){return fa(cz["cg"]["j"]["slice"](cy,cy+cA))}this[dF(3667,13)](cy,cH,Object[dF(3779,8)](cy[fb(3793,11)]||null),dJ<<1|1)}else{function fc(cy){function cA(cA,cE={["bS"]:{}},df){while(cC(cA)!==237){switch(cC(cA)){case cA[75]-1156:case-876:return cH=true,(1,cz["cg"]["d"])(cE["bS"]["d"]);cA[16]+=cA[9]- -464,cA[19]+=cA[82]- -1896,cA[24]+=cA[50]-722,cA[27]+=cA[19]- -1509,cA[41]+=cA[40]- -363,cA[51]+=cA[88]-1158,cA[52]+=cA[51]-822,cA[82]+=cA[76]- -1344,cA[83]+=cA[66]- -1590;break;case-284:case cA[61]- -1371:if(!(cA[cA[27]+81]==cA[85]+1577)){cA[16]+=cA[71]- -1413,cA[19]+=cA[39]-1221,cA[24]+=cA[92]- -664,cA[27]+=cA[88]-3452,cA[41]+=cA[63]- -428,cA[51]+=cA[25]- -2351,cA[52]+=cA[28]- -912,cA[82]+=cA[50]- -165,cA[83]+=cA[58]-1316;break}if(!(cA[47]==525)){cA[16]+=cA[63]- -2338,cA[19]+=cA[75]-1251,cA[24]+=cA[14]- -1540,cA[27]+=cA[98]-1700,cA[41]+=cA[11]-504,cA[51]+=cA[79]- -2381,cA[52]+=cA[90]- -227,cA[82]+=cA[55]-307,cA[83]+=cA[92]-1884;break}cA[16]+=cA[65]- -2320,cA[19]+=cA[4]-1469,cA[24]+=cA[59]- -38,cA[27]+=cA[4]-228,cA[41]+=cA[20]-1136,cA[51]+=cA[58]-149,cA[52]+=cA[25]-44,cA[82]+=cA[76]- -717,cA[83]+=cA[13]- -785;break;case-38:case-756:case 570:cE["bS"]["d"].push((cE["bS"]["e"]|cE["bS"]["g"]<-408)){cA[16]+=cA[94]-949,cA[19]+=cA[9]-1413,cA[24]+=cA[46]- -669,cA[27]+=cA[94]-4807,cA[41]+=cA[69]-380,cA[51]+=cA[46]- -1725,cA[52]+=cA[86]- -1270,cA[82]+=cA[34]- -1203,cA[83]+=cA[41]-19;break}cA[16]+=cA[17]-86,cA[19]+=cA[17]-2605,cA[24]+=cA[68]- -624,cA[27]+=cA[53]-1266,cA[41]+=cA[34]-386,cA[51]+=cA[90]- -1631,cA[52]+=cA[57]- -1687,cA[82]+=cA[65]- -1961,cA[83]+=cA[54]-485;break;case 498:case cA[46]-40:cE["bS"]["d"].push((cE["bS"]["e"]|cE["bS"]["g"]<-(cA[73]+1145))){cA[16]+=cA[78]- -683,cA[19]+=cA[67]- -453,cA[24]+=cA[8]- -2804,cA[27]+=cA[64]-3489,cA[41]+=cA[4]- -944,cA[51]+=cA[14]- -1333,cA[52]+=cA[25]- -1373,cA[82]+=cA[14]-1000,cA[83]+=cA[90]-869;break}cA[16]+=cA[29]- -81,cA[19]+=cA[21]-2919,cA[24]+=cA[11]- -2907,cA[27]+=cA[71]- -732,cA[41]+=cA[86]- -844,cA[51]+=cA[60]-58,cA[52]+=cA[41]- -232,cA[82]+=cA[32]-2587,cA[83]+=cA[24]-1975;break;case cA[6]- -812:cE["bS"]["c"]=cE["bS"]["b"].length;cE["bS"]["d"]=[];cE["bS"]["e"]=cA[47]+-525;cE["bS"]["f"]=0;cE["bS"]["g"]=-(cA[31]+332);if(!(cA[75]!=-(cA[55]+109))){cA[16]+=cA[27]- -640,cA[19]+=cA[23]-1097,cA[24]+=cA[35]- -1014,cA[27]+=cA[40]-334,cA[41]+=cA[36]-495,cA[51]+=cA[8]- -688,cA[52]+=cA[22]-2526,cA[82]+=cA[18]- -792,cA[83]+=cA[30]- -211;break}cA[16]+=cA[93]- -372,cA[19]+=cA[57]- -636,cA[24]+=cA[7]- -1979,cA[27]+=cA[64]-1330,cA[41]+=cA[60]-3357,cA[51]+=cA[83]- -28,cA[52]+=cA[72]-395,cA[82]+=cA[39]-722,cA[83]+=cA[6]- -157;break;case-587:case 827:case-501:for(cE["bS"]["h"]=0;cE["bS"]["h"]cA[11]+919?13:14;do{cE["bS"]["d"].push(cE["bS"]["e"]&cA[10]+243);cE["bS"]["e"]>>=cA[48]+-273;cE["bS"]["f"]-=8}while(cE["bS"]["f"]>7);cE["bS"]["g"]=-1}}if(cE["bS"]["g"]>-(cA[20]+-502)){cA[16]+=cA[53]- -237,cA[19]+=cA[39]-388,cA[24]+=cA[89]- -291,cA[27]+=cA[5]- -494,cA[41]+=cA[91]- -3231,cA[51]+=cA[38]- -499,cA[52]+=cA[89]-545,cA[82]+=cA[24]-2399,cA[83]+=cA[87]-371;break}else{cA[16]+=cA[85]-63,cA[19]+=cA[88]-2915,cA[24]+=cA[8]- -734,cA[27]+=cA[36]-1404,cA[41]+=cA[65]- -3837,cA[51]+=cA[66]- -1807,cA[52]+=cA[22]-681,cA[82]+=cA[41]-1190,cA[83]+=cA[28]-478;break}case 406:case cA[93]- -1110:case-613:cE["bS"]["a"]=cB(cA[14]+69254,12567,91);cE["bS"]["b"]=""+(cy||"");cA[16]+=cA[46]-465,cA[19]+=cA[21]- -289,cA[24]+=cA[77]-1152,cA[27]+=cA[61]- -1700,cA[41]+=cA[20]-183,cA[51]+=cA[87]-1241,cA[52]+=cA[41]- -1951,cA[82]+=cA[62]-1028,cA[83]+=cA[42]- -1397;break;case 904:case-286:if(cA[21]<-928){cA[16]+=cA[23]- -1011,cA[19]+=cA[81]- -926,cA[24]+=cA[89]-109,cA[27]+=cA[12]- -3722,cA[41]+=cA[65]- -404,cA[51]+=cA[31]-1291,cA[52]+=cA[15]-698,cA[82]+=cA[11]-1298,cA[83]+=cA[98]-1097;break}cA[16]+=cA[89]- -985,cA[19]+=cA[88]-700,cA[24]+=cA[81]- -710,cA[27]+=cA[98]- -1732,cA[41]+=cA[78]-712,cA[51]+=cA[34]-1095,cA[52]+=cA[21]-1768,cA[82]+=cA[79]- -499,cA[83]+=cA[17]- -614;break}}}var cH;var cE=cA([...cD(0,16),444,...cD(17,19),-622,...cD(20,24),1213,...cD(25,27),-863,...cD(28,41),-608,...cD(42,51),827,-704,...cD(53,82),-152,-659,...cD(84,99)]);if(cH){return cE}}function fd(cy,cA){return fc(cz["cg"]["j"]["slice"](cy,cy+cA))}dl[dG+dJ]=Reflect[fd(3807,11)](eT,cH)}break}case cz["cg"]["y"][dF(3820,8)]:{function fe(cy){function cA(cA,cE={["bU"]:{}},df){while(cC(cA)!==-480){switch(cC(cA)){case-987:case 855:case cA[65]- -1332:return cH=true,(1,cz["cg"]["d"])(cE["bU"]["d"]);if(cA[30]==-857){cA[18]+=cA[76]- -4630,cA[31]+=cA[5]-400,cA[34]+=cA[46]-718,cA[37]+=cA[60]-2825,cA[38]+=cA[49]-2858,cA[40]+=cA[67]- -804,cA[52]+=cA[51]-1697,cA[69]+=cA[38]- -2903,cA[71]+=cA[29]- -265;break}cA[18]+=cA[44]- -4240,cA[31]+=cA[76]-1410,cA[34]+=cA[31]-251,cA[37]+=cA[54]-1952,cA[38]+=cA[39]-916,cA[40]+=cA[9]-988,cA[52]+=cA[46]-2093,cA[69]+=cA[76]-318,cA[71]+=cA[23]- -839;break;case-173:case-49:cA[18]+=cA[61]- -1417,cA[31]+=cA[48]- -1461,cA[34]+=cA[7]- -1973,cA[37]+=cA[15]-195,cA[38]+=cA[31]-882,cA[40]+=cA[2]- -2487,cA[52]+=cA[23]- -287,cA[69]+=cA[9]-4078,cA[71]+=cA[47]-1358;break;case cA[1]-416:case-558:case 577:cE["bU"]["a"]=cB(cA[1]+323177,12763,91);cE["bU"]["b"]=""+(cy||"");cE["bU"]["c"]=cE["bU"]["b"].length;cE["bU"]["d"]=[];cA[18]+=cA[27]- -1146,cA[31]+=cA[34]- -1455,cA[34]+=cA[76]-61,cA[37]+=cA[64]-818,cA[38]+=cA[45]-1624,cA[40]+=cA[43]-235,cA[52]+=cA[3]-1867,cA[69]+=cA[33]- -1625,cA[71]+=cA[63]- -849;break;case 297:case-572:case 352:if(cA[cA[73]+783]<-(cA[63]+1514)){cA[18]+=cA[48]- -1098,cA[31]+=cA[27]- -835,cA[34]+=cA[61]- -4467,cA[37]+=cA[31]-2435,cA[38]+=cA[41]-2788,cA[40]+=cA[25]- -1100,cA[52]+=cA[6]- -11,cA[69]+=cA[22]-871,cA[71]+=cA[39]-1125;break}if(cA[43]!=cA[42]+1725){cA[18]+=cA[13]- -1439,cA[31]+=cA[13]-772,cA[34]+=cA[51]- -2782,cA[37]+=cA[6]-1103,cA[38]+=cA[30]-1202,cA[40]+=cA[71]-369,cA[52]+=cA[43]-2266,cA[69]+=cA[8]- -177,cA[71]+=cA[60]-1258;break}cA[18]+=cA[34]- -5664,cA[31]+=cA[74]-1289,cA[34]+=cA[66]- -4226,cA[37]+=cA[25]-1457,cA[38]+=cA[48]-1598,cA[40]+=cA[63]- -929,cA[52]+=cA[1]- -1835,cA[69]+=cA[8]-2079,cA[71]+=cA[43]-1008;break;case cA[73]- -340:case 755:case 716:cE["bU"]["e"]=cA[1]+487;cE["bU"]["f"]=0;cA[18]+=cA[50]-1531,cA[31]+=cA[6]-233,cA[34]+=cA[9]- -3,cA[37]+=cA[33]-432,cA[38]+=cA[40]- -3800,cA[40]+=cA[53]-400,cA[52]+=cA[64]-582,cA[69]+=cA[13]-4083,cA[71]+=cA[2]- -1461;break;case cA[11]-42:case-753:cE["bU"]["g"]=-(cA[61]+611);for(cE["bU"]["h"]=cA[47]+-525;cE["bU"]["h"]88?cA[64]+-908:14;do{cE["bU"]["d"].push(cE["bU"]["e"]&255);cE["bU"]["e"]>>=8;cE["bU"]["f"]-=8}while(cE["bU"]["f"]>7);cE["bU"]["g"]=-(cA[72]+-27)}}if(cE["bU"]["g"]>-1){cA[18]+=cA[21]-1438,cA[31]+=cA[19]-960,cA[34]+=cA[51]-1758,cA[37]+=cA[65]- -882,cA[38]+=cA[47]-798,cA[40]+=cA[47]- -3,cA[52]+=cA[69]- -3343,cA[69]+=cA[72]- -3900,cA[71]+=cA[66]-517;break}else{cA[18]+=cA[66]-2775,cA[31]+=cA[52]- -425,cA[34]+=cA[22]-784,cA[37]+=cA[11]- -2362,cA[38]+=cA[3]-1308,cA[40]+=cA[69]- -3705,cA[52]+=cA[53]- -453,cA[69]+=cA[20]- -4033,cA[71]+=cA[47]-1226;break}case cA[4]-153:cE["bU"]["d"].push((cE["bU"]["e"]|cE["bU"]["g"]<>1):0]=fg;break}case cz["cg"]["y"][dF(3940,7)]:throw dl[dG+this[dF(2578,10)]()];case cz["cg"]["y"][dF(3955,15)]:{var dJ=this[dF(2578,10)]();var fk=this[dF(2578,10)]();var fl=this[dF(2578,10)]();var fm=this[dF(2578,10)]();var fn=this[dF(2578,10)]();var fo=this[dF(2578,10)]();var fp=new Array(fn);for(var dy=0;dy88?13:cA[66]+413;do{cE["bW"]["d"].push(cE["bW"]["e"]&255);cE["bW"]["e"]>>=cA[14]+-97;cE["bW"]["f"]-=cA[65]+616}while(cE["bW"]["f"]>7);cE["bW"]["g"]=-1}}if(cE["bW"]["g"]>-(cA[42]+880)){cA[10]+=cA[20]-978,cA[11]+=cA[46]- -1452,cA[28]+=cA[3]-1852,cA[37]+=cA[26]-1021,cA[38]+=cA[36]-645,cA[44]+=cA[76]-232,cA[72]+=cA[31]- -1425,cA[76]+=cA[4]- -223;break}else{cA[10]+=cA[43]-1648,cA[11]+=cA[19]- -2255,cA[28]+=cA[1]- -77,cA[37]+=cA[14]- -224,cA[38]+=cA[32]-1123,cA[44]+=cA[15]- -307,cA[72]+=cA[65]-1840,cA[76]+=cA[60]-203;break}case-879:if(!(cA[39]>cA[20]+-30)){cA[10]+=cA[66]-53,cA[11]+=cA[79]- -1866,cA[28]+=cA[35]- -2712,cA[37]+=cA[80]-225,cA[38]+=cA[41]- -340,cA[44]+=cA[53]- -318,cA[72]+=cA[46]-3416,cA[76]+=cA[64]-112;break}cA[10]+=cA[58]-13,cA[11]+=cA[77]-170,cA[28]+=cA[56]- -3854,cA[37]+=cA[72]- -9,cA[38]+=cA[26]-860,cA[44]+=cA[81]- -753,cA[72]+=cA[9]-2043,cA[76]+=cA[2]- -922;break;case 394:case-367:case cA[6]-706:if(cA[17]<-789){cA[10]+=cA[26]-428,cA[11]+=cA[15]- -1,cA[28]+=cA[45]- -789,cA[37]+=cA[3]-778,cA[38]+=cA[16]- -1124,cA[44]+=cA[62]-791,cA[72]+=cA[73]- -584,cA[76]+=cA[42]- -1395;break}cA[10]+=cA[62]-3440,cA[11]+=cA[53]- -195,cA[28]+=cA[61]- -1393,cA[37]+=cA[2]- -841,cA[38]+=cA[64]- -745,cA[44]+=cA[70]- -199,cA[72]+=cA[58]-975,cA[76]+=cA[29]- -1663;break;case cA[18]-20:case 218:cA[10]+=cA[79]- -910,cA[11]+=cA[31]- -825,cA[28]+=cA[77]- -1656,cA[37]+=cA[8]- -1832,cA[38]+=cA[80]-1090,cA[44]+=cA[35]-106,cA[72]+=cA[56]-2537,cA[76]+=cA[49]- -1448;break;case 791:if(!(cA[2]cA[48]+-193?cA[16]+-255:14;do{cE["bY"]["d"].push(cE["bY"]["e"]&255);cE["bY"]["e"]>>=cA[43]+-838;cE["bY"]["f"]-=8}while(cE["bY"]["f"]>cA[14]+-98);cE["bY"]["g"]=-1}}if(cE["bY"]["g"]>-(cA[2]+893)){cA[0]+=cA[6]-148,cA[15]+=cA[14]-441,cA[16]+=cA[22]-5070,cA[23]+=cA[11]- -452,cA[67]+=cA[27]- -5392,cA[72]+=cA[75]- -525,cA[80]+=cA[56]- -2069,cA[85]+=cA[77]- -1060;break}else{cA[0]+=cA[23]-2078,cA[15]+=cA[41]- -485,cA[16]+=cA[42]- -862,cA[23]+=cA[31]- -589,cA[67]+=cA[28]- -5239,cA[72]+=cA[5]- -398,cA[80]+=cA[27]-2936,cA[85]+=cA[64]-1054;break}if(!(cA[59]==cA[72]+469)){cA[0]+=cA[65]-1234,cA[15]+=cA[57]- -517,cA[16]+=cA[38]- -411,cA[23]+=cA[68]- -306,cA[67]+=cA[9]- -4531,cA[72]+=cA[5]-2207,cA[80]+=cA[28]-334,cA[85]+=cA[14]-1466;break}case cA[59]-877:cA[0]+=cA[38]- -269,cA[15]+=cA[63]- -1640,cA[16]+=cA[17]- -1424,cA[23]+=cA[85]- -667,cA[67]+=cA[8]- -1322,cA[72]+=cA[5]- -1350,cA[80]+=cA[7]-4310,cA[85]+=cA[71]- -1302;break;case cA[78]-562:if(cA[cA[46]+124]>433){cA[0]+=cA[77]- -2685,cA[15]+=cA[35]- -398,cA[16]+=cA[34]-1138,cA[23]+=cA[25]- -434,cA[67]+=cA[62]-6399,cA[72]+=cA[29]- -2009,cA[80]+=cA[70]-89,cA[85]+=cA[18]- -1177;break}cA[0]+=cA[47]- -1195,cA[15]+=cA[43]-5329,cA[16]+=cA[51]-1383,cA[23]+=cA[68]- -331,cA[67]+=cA[74]-951,cA[72]+=cA[39]- -1046,cA[80]+=cA[7]- -1006,cA[85]+=cA[78]- -641;break;case cA[18]- -456:cE["bY"]["d"].push((cE["bY"]["e"]|cE["bY"]["g"]<263){cA[1]+=cA[57]- -274,cA[5]+=cA[56]- -2587,cA[22]+=cA[57]-53,cA[33]+=cA[48]-595,cA[38]+=cA[29]-604,cA[72]+=cA[38]- -597,cA[76]+=cA[27]- -1710,cA[93]+=cA[72]-700,cA[95]+=cA[24]- -216;break}cA[1]+=cA[38]- -283,cA[5]+=cA[89]- -2394,cA[22]+=cA[74]-1389,cA[33]+=cA[41]-443,cA[38]+=cA[80]-467,cA[72]+=cA[39]-807,cA[76]+=cA[23]- -1745,cA[93]+=cA[72]-1161,cA[95]+=cA[9]-209;break;case cA[69]-928:cA[1]+=cA[9]-604,cA[5]+=cA[26]-1826,cA[22]+=cA[54]- -1330,cA[33]+=cA[14]- -551,cA[38]+=cA[16]- -57,cA[72]+=cA[82]-247,cA[76]+=cA[35]-1215,cA[93]+=cA[5]- -1962,cA[95]+=cA[92]-1213;break;case 50:case cA[86]- -1090:cE["ca"]["g"]=-(cA[31]+332);for(cE["ca"]["h"]=cA[43]+-846;cE["ca"]["h"]cA[80]+-723?cA[91]+555:14;do{cE["ca"]["d"].push(cE["ca"]["e"]&255);cE["ca"]["e"]>>=8;cE["ca"]["f"]-=cA[90]+531}while(cE["ca"]["f"]>cA[5]+-55);cE["ca"]["g"]=-1}}if(cE["ca"]["g"]>-1){cA[1]+=cA[7]- -178,cA[5]+=cA[79]-90,cA[22]+=cA[7]- -75,cA[33]+=cA[21]-1061,cA[38]+=cA[88]- -246,cA[72]+=cA[71]- -522,cA[76]+=cA[45]- -368,cA[93]+=cA[11]- -2645,cA[95]+=cA[61]- -409;break}else{cA[1]+=cA[80]-1348,cA[5]+=cA[62]-1157,cA[22]+=cA[25]- -204,cA[33]+=cA[59]- -461,cA[38]+=cA[95]-1292,cA[72]+=cA[42]- -451,cA[76]+=cA[40]- -849,cA[93]+=cA[81]- -518,cA[95]+=cA[23]- -997;break}case-579:case cA[26]-1148:case-398:return cH=true,(1,cz["cg"]["d"])(cE["ca"]["d"]);if(cA[cA[13]+409]>cA[20]+-207){;break}if(cA[cA[0]+-641]<-(cA[47]+55)){cA[1]+=cA[36]- -490,cA[5]+=cA[9]-1207,cA[22]+=cA[91]- -1875,cA[33]+=cA[7]- -58,cA[38]+=cA[56]-22,cA[72]+=cA[4]- -21,cA[76]+=cA[24]-1701,cA[93]+=cA[47]- -1225,cA[95]+=cA[46]- -9;break}cA[1]+=cA[32]-2175,cA[5]+=cA[84]-1221,cA[22]+=cA[40]- -1564,cA[33]+=cA[1]- -2540,cA[38]+=cA[86]- -1180,cA[72]+=cA[91]- -1260,cA[76]+=cA[95]-1246,cA[93]+=cA[37]- -2410,cA[95]+=cA[10]-466;break;case-752:case 537:cA[1]+=cA[58]-174,cA[5]+=cA[49]-52,cA[22]+=cA[78]- -161,cA[33]+=cA[23]- -1422,cA[38]+=cA[22]-2279,cA[72]+=cA[87]-877,cA[76]+=cA[84]- -1398,cA[93]+=cA[41]- -1326,cA[95]+=cA[15]- -1491;break;case 412:cA[1]+=cA[27]-614,cA[5]+=cA[49]-1538,cA[22]+=cA[35]-86,cA[33]+=cA[92]- -1245,cA[38]+=cA[22]- -262,cA[72]+=cA[85]- -1668,cA[76]+=cA[14]-1549,cA[93]+=cA[53]- -121,cA[95]+=cA[65]- -878;break;case 984:case-791:cA[1]+=cA[48]-1103,cA[5]+=cA[71]-684,cA[22]+=cA[44]- -60,cA[33]+=cA[15]- -1445,cA[38]+=cA[28]-788,cA[72]+=cA[68]- -989,cA[76]+=cA[16]-722,cA[93]+=cA[56]- -814,cA[95]+=cA[78]- -366;break;case cA[64]-1024:cA[1]+=cA[74]-37,cA[5]+=cA[97]-3407,cA[22]+=cA[60]-2163,cA[33]+=cA[81]- -470,cA[38]+=cA[60]-1464,cA[72]+=cA[92]-1484,cA[76]+=cA[80]-932,cA[93]+=cA[74]- -1977,cA[95]+=cA[11]- -1839;break}}}var cH;var cE=cA([656,759,...cD(2,5),-931,...cD(6,22),-282,...cD(23,33),-53,...cD(34,38),72,...cD(39,72),-444,...cD(73,76),-1957,...cD(77,93),570,683,701,...cD(96,99)]);if(cH){return cE}}function fI(cy,cA){return fH(cz["cg"]["j"]["slice"](cy,cy+cA))}var dS=dl[dG+this[dF(2578,10)]()];var dT=dl[dG+this[dF(2578,10)]()];var fJ=dl[dG+this[dF(2578,10)]()];var fF=Object[dF(4280,30)](dS,dT);var fK={[fI(4409,4)]:fJ,[fI(4414,15)]:true,[fI(4435,13)]:true};if(fF&&typeof fF[fI(4448,4)]===fI(4454,10)){fK[fI(4448,4)]=fF[fI(4448,4)]}Object[fI(4467,18)](dS,dT,fK);break}case cz["cg"]["y"][dF(4489,15)]:{var dJ=this[dF(2578,10)]();var dS=dl[dG+this[dF(2578,10)]()];var fL=[];if(dS!==null&&dS!==undefined){var fM=Object[dF(3779,8)](null);var fN=Object(dS);while(fN!==null){var fO=Object[dF(4510,24)](fN);for(var dy=0;dy=fR[dF(4561,7)][dF(4060,8)]){dl[dk+cz["cg"]["A"]["PC"]]=fS}else{dl[dG+dJ]=fR[dF(4561,7)][fR["i"]++]}break}case cz["cg"]["y"][dF(4595,11)]:{var fT=dl[dk+cz["cg"]["A"][dF(4611,10)]];if(!fT){dl[dk+cz["cg"]["A"][dF(4611,10)]]=fT=[]}fT[dF(4125,5)]({[dF(4624,11)]:this[dF(2578,10)](),[dF(4639,15)]:this[dF(2578,10)]()});break}case cz["cg"]["y"][dF(4661,9)]:{dl[dk+cz["cg"]["A"][dF(4611,10)]][dF(4673,4)]();break}case cz["cg"]["y"][dF(4681,16)]:{var fT=dl[dk+cz["cg"]["A"][dF(4611,10)]];if(!fT){dl[dk+cz["cg"]["A"][dF(4611,10)]]=fT=[]}fT[dF(4125,5)]({[dF(4698,11)]:this[dF(2578,10)](),[dF(4712,9)]:this[dF(2578,10)](),[dF(4723,13)]:this[dF(2578,10)](),[dF(4738,10)]:this[dF(2578,10)]()});break}case cz["cg"]["y"][dF(4753,7)]:{function fU(cy){function cA(cA,cE={["cc"]:{}},df){while(cC(cA)!==710){switch(cC(cA)){case cA[71]-180:case-525:if(cA[cA[64]+-877]>cA[44]+0){cA[0]+=cA[50]-362,cA[2]+=cA[19]- -3561,cA[8]+=cA[9]-24,cA[19]+=cA[45]- -1327,cA[27]+=cA[63]- -639,cA[40]+=cA[51]-1865,cA[42]+=cA[44]- -571,cA[50]+=cA[13]-92,cA[52]+=cA[50]-2202,cA[70]+=cA[73]- -241;break}cA[0]+=cA[5]-561,cA[2]+=cA[34]- -1806,cA[8]+=cA[45]- -1409,cA[19]+=cA[59]-28,cA[27]+=cA[44]-529,cA[40]+=cA[16]-1201,cA[42]+=cA[59]-405,cA[50]+=cA[18]-671,cA[52]+=cA[31]- -614,cA[70]+=cA[62]-1070;break;case cA[81]- -801:case-913:cE["cc"]["a"]=cB(cA[4]+744766,13523,91);cE["cc"]["b"]=""+(cy||"");cE["cc"]["c"]=cE["cc"]["b"].length;cA[0]+=cA[11]- -820,cA[2]+=cA[56]-438,cA[8]+=cA[25]-194,cA[19]+=cA[44]- -593,cA[27]+=cA[17]- -1357,cA[40]+=cA[33]-1112,cA[42]+=cA[28]-451,cA[50]+=cA[16]-111,cA[52]+=cA[24]- -831,cA[70]+=cA[62]-1450;break;case cA[63]- -225:cE["cc"]["d"]=[];cE["cc"]["e"]=cA[54]+-904;cE["cc"]["f"]=0;cA[0]+=cA[1]- -1775,cA[2]+=cA[50]- -979,cA[8]+=cA[35]- -109,cA[19]+=cA[79]- -1828,cA[27]+=cA[12]-1704,cA[40]+=cA[70]-3292,cA[42]+=cA[30]-1810,cA[50]+=cA[30]- -17,cA[52]+=cA[80]- -1770,cA[70]+=cA[48]- -716;break;case cA[43]-750:cE["cc"]["g"]=-(cA[2]+-7);for(cE["cc"]["h"]=cA[62]+-896;cE["cc"]["h"]cA[59]+-359?cA[56]+924:cA[87]+413;do{cE["cc"]["d"].push(cE["cc"]["e"]&255);cE["cc"]["e"]>>=8;cE["cc"]["f"]-=8}while(cE["cc"]["f"]>cA[33]+-737);cE["cc"]["g"]=-(cA[12]+-401)}}if(cE["cc"]["g"]>-(cA[53]+87)){cA[0]+=cA[68]- -40,cA[2]+=cA[18]-5167,cA[8]+=cA[79]- -1136,cA[19]+=cA[86]- -89,cA[27]+=cA[25]- -1225,cA[40]+=cA[63]- -6062,cA[42]+=cA[29]- -449,cA[50]+=cA[38]-920,cA[52]+=cA[14]-379,cA[70]+=cA[35]-377;break}else{cA[0]+=cA[32]-2062,cA[2]+=cA[79]- -190,cA[8]+=cA[46]- -8,cA[19]+=cA[52]-1021,cA[27]+=cA[57]- -1997,cA[40]+=cA[8]- -3211,cA[42]+=cA[52]- -639,cA[50]+=cA[7]-309,cA[52]+=cA[81]- -830,cA[70]+=cA[72]-703;break}case-826:if(!(cA[cA[86]+253]==cA[1]+1286)){cA[0]+=cA[74]- -41,cA[2]+=cA[54]-893,cA[8]+=cA[29]- -50,cA[19]+=cA[22]-890,cA[27]+=cA[76]- -1511,cA[40]+=cA[70]-1252,cA[42]+=cA[87]- -1866,cA[50]+=cA[38]- -851,cA[52]+=cA[86]- -1718,cA[70]+=cA[79]-3729;break}cA[0]+=cA[24]-72,cA[2]+=cA[81]- -627,cA[8]+=cA[33]- -223,cA[19]+=cA[31]- -1685,cA[27]+=cA[69]-601,cA[40]+=cA[38]-4212,cA[42]+=cA[40]- -4556,cA[50]+=cA[48]- -515,cA[52]+=cA[46]- -1932,cA[70]+=cA[60]-1834;break;case-546:cE["cc"]["d"].push((cE["cc"]["e"]|cE["cc"]["g"]<>>13))>>>0}break}case cz["cg"]["y"][dF(4793,10)]:{dl[dk+cz["cg"]["A"]["PC"]]=dl[dG+this[dF(2578,10)]()];break}case cz["cg"]["y"][dF(4805,10)]:{debugger;break}default:throw new Error(dF(4821,20)+dp+dF(4845,9)+(dl[dk+cz["cg"]["A"]["PC"]]-1))}}catch(err){var gb=0;var gc=this["_f"];while(gc){var gd=dl[gc+cz["cg"]["A"][dj(4860,10)]];if(gd&&gd[dj(2292,8)]>0){gb=gc;break}this[dj(4875,21)](gc);this[dj(4901,10)]=gc;gc=dl[gc+cz["cg"]["A"][dj(4916,8)]];this["_f"]=gc}if(!gb){throw err}var ge=dl[gb+cz["cg"]["A"][dj(4860,10)]][dj(4927,4)]();var gf=dl[gb+cz["cg"]["A"][dj(4932,10)]];if(ge[dj(4943,15)]!==undefined){dl[gf+ge[dj(4943,15)]]=err;dl[gb+cz["cg"]["A"]["PC"]]=ge[dj(4960,11)]}else{dl[gf+ge[dj(4973,9)]]=ge[dj(4984,10)];dl[gf+ge[dj(4998,13)]]=err;dl[gb+cz["cg"]["A"]["PC"]]=ge[dj(5015,11)]}this[dj(4901,10)]=gb+dl[gb+cz["cg"]["A"][dj(5030,13)]];this["_f"]=gb}if(!this["_f"]){return this[dj(2266,7)][0]}}};cy[2]+=cy[36]-1790,cy[16]+=cy[22]-533,cy[28]+=cy[44]-1261,cy[46]+=cy[5]-1742,cy[48]+=cy[64]-2142,cy[50]+=cy[70]-1741,cy[54]+=cy[73]- -1204,cy[63]+=cy[78]- -1315,cy[67]+=cy[49]-12,cy[76]+=cy[74]- -3392;break;case-630:case cy[61]- -1562:case-142:cz["ct"]={};cz["ct"]["b"]=function(...cy){return cE([...cD(0,2),-499,...cD(3,16),450,...cD(17,28),-842,...cD(29,46),730,525,458,-381,-90,...cD(51,54),547,...cD(55,63),844,...cD(64,67),-2766,...cD(68,76),-134,...cD(77,79)],{["ct"]:cz["ct"],["cg"]:cz["cg"],["cx"]:{}},cA,cy)};cz["ct"]["a"]=function(...cy){return cE([...cD(0,2),-435,...cD(3,16),-1034,...cD(17,28),-221,...cD(29,46),-300,525,418,-381,265,...cD(51,54),-662,...cD(55,63),-15,...cD(64,67),-688,...cD(68,76),727,...cD(77,79)],{["ct"]:cz["ct"],["cg"]:cz["cg"],["cu"]:{}},cA,cy)};cz["cg"]["D"][(1,cz["cg"]["b"])(5076,cy[49]+389)]=module;cy[2]+=cy[68]-1564,cy[16]+=cy[68]- -57,cy[28]+=cy[58]- -462,cy[46]+=cy[76]-2436,cy[48]+=cy[58]- -1050,cy[50]+=cy[20]- -1501,cy[54]+=cy[6]-443,cy[63]+=cy[19]-71,cy[67]+=cy[30]-595,cy[76]+=cy[15]-1007;break;case cy[77]- -312:case-55:case-505:cz["cg"]["A"]={["PC"]:cy[40]+835,[(cy[12]+-401,cz["cg"]["b"])(cy[20]+481,8)]:cy[13]+340,[(1,cz["cg"]["b"])(996,cy[42]+888)]:cy[34]+-924,[(cy[25]+486,cz["cg"]["b"])(cy[16]+612,5)]:3,[(cy[64]+-920,cz["cg"]["b"])(cy[2]+1271,cy[70]+-287)]:cy[21]+-429,[(1,cz["cg"]["b"])(cy[62]+136,cy[77]+853)]:cy[39]+-882,[(1,cz["cg"]["b"])(cy[59]+602,13)]:cy[10]+-6,[(cy[71]+529,cz["cg"]["b"])(cy[37]+1964,cy[58]+-353)]:cy[9]+-965};cz["cg"]["B"]=8;cz["cg"]["k"]=cy[43]+-845;cy[2]+=cy[23]- -1258,cy[16]+=cy[14]-1128,cy[28]+=cy[38]-709,cy[46]+=cy[52]- -92,cy[48]+=cy[16]- -625,cy[50]+=cy[14]-1358,cy[54]+=cy[23]-62,cy[63]+=cy[21]- -6476,cy[67]+=cy[29]-1180,cy[76]+=cy[23]- -63;break;case 890:[cz["cl"]["a"]]=cH;if(typeof cz["cg"]["m"]!==cB(cy[12]+130110,13879,9)&&cz["cg"]["m"]){return new cz["cg"]["m"]()[cB(cy[64]+175010,13895,6)](new cz["cg"]["n"](cz["cl"]["a"]))}else if(typeof cz["cg"]["o"]!==cB(cy[67]+618265,13906,9)&&cz["cg"]["o"]){return cz["cg"]["o"][cB(cy[72]+916071,13921,4)](cz["cl"]["a"])[cB(cy[31]+99724,13930,8)](cB(cy[43]+427866,13941,5))}else{return(1,cz["cg"]["r"])(cz["cl"]["a"])}return undefined;case 414:case-760:case 642:cz["cg"]["i"][(1,cz["cg"]["b"])(cy[61]+1813,cy[1]+498)][(1,cz["cg"]["b"])(1546,13)]=function(cy,cA,cH,cE){function gg(cy){function cA(cA,cE={["S"]:{}},gg){while(cC(cA)!==-816){switch(cC(cA)){case-941:cE["S"]["d"].push((cE["S"]["e"]|cE["S"]["g"]<88?13:cA[1]+501;do{cE["S"]["d"].push(cE["S"]["e"]&cA[0]+-401);cE["S"]["e"]>>=8;cE["S"]["f"]-=8}while(cE["S"]["f"]>7);cE["S"]["g"]=-1}}if(cE["S"]["g"]>-1){cA[8]+=cA[56]-5035,cA[24]+=cA[43]-485,cA[28]+=cA[68]-287,cA[42]+=cA[28]- -1132,cA[69]+=cA[81]- -1681,cA[74]+=cA[64]- -861,cA[76]+=cA[69]- -1164,cA[85]+=cA[84]-613,cA[95]+=cA[25]-4;break}else{cA[8]+=cA[19]-1455,cA[24]+=cA[15]-605,cA[28]+=cA[92]-922,cA[42]+=cA[57]- -1248,cA[69]+=cA[92]- -427,cA[74]+=cA[64]- -577,cA[76]+=cA[75]- -15,cA[85]+=cA[93]- -926,cA[95]+=cA[69]-1114;break}case 843:case-475:if(!(cA[cA[36]+-806]==-(cA[71]+1056))){cA[8]+=cA[36]- -111,cA[24]+=cA[91]- -186,cA[28]+=cA[61]-1596,cA[42]+=cA[25]- -1449,cA[69]+=cA[37]- -454,cA[74]+=cA[88]-287,cA[76]+=cA[11]- -786,cA[85]+=cA[35]- -2787,cA[95]+=cA[0]-2239;break}case cA[74]-188:case 776:case 951:return cH=true,(1,cz["cg"]["d"])(cE["S"]["d"]);if(cA[cA[72]+-2]<628){cA[8]+=cA[63]-275,cA[24]+=cA[55]- -54,cA[28]+=cA[63]-685,cA[42]+=cA[45]- -807,cA[69]+=cA[56]- -1628,cA[74]+=cA[90]- -328,cA[76]+=cA[86]- -2134,cA[85]+=cA[43]-547,cA[95]+=cA[41]- -877;break}cA[8]+=cA[42]- -571,cA[24]+=cA[68]- -1100,cA[28]+=cA[43]-4356,cA[42]+=cA[91]- -1686,cA[69]+=cA[80]-1661,cA[74]+=cA[18]-355,cA[76]+=cA[81]- -1281,cA[85]+=cA[61]- -1037,cA[95]+=cA[71]- -408;break;case cA[84]-908:case-803:if(!(cA[cA[61]+704]==cA[12]+281)){;break}cA[8]+=cA[37]- -2562,cA[24]+=cA[89]- -1191,cA[28]+=cA[89]-1154,cA[42]+=cA[66]- -80,cA[69]+=cA[48]-441,cA[74]+=cA[91]- -156,cA[76]+=cA[60]-2098,cA[85]+=cA[60]-690,cA[95]+=cA[5]- -633;break;case cA[11]- -463:if(!(cA[48]==cA[81]+766)){cA[8]+=cA[11]- -2255,cA[24]+=cA[33]-2105,cA[28]+=cA[27]- -109,cA[42]+=cA[38]-763,cA[69]+=cA[60]-346,cA[74]+=cA[9]-788,cA[85]+=cA[83]- -348,cA[95]+=cA[16]- -727;break}cA[8]+=cA[13]-3107,cA[24]+=cA[11]- -1151,cA[28]+=cA[26]-46,cA[42]+=cA[8]- -3008,cA[69]+=cA[14]- -369,cA[74]+=cA[36]-323,cA[76]+=cA[43]- -449,cA[85]+=cA[0]- -34,cA[95]+=cA[3]-626;break;case cA[43]- -117:case 495:case-95:cE["S"]["d"]=[];cE["S"]["e"]=cA[59]+-447;cA[8]+=cA[53]- -2801,cA[24]+=cA[20]- -120,cA[28]+=cA[74]- -1340,cA[42]+=cA[12]-1095,cA[69]+=cA[62]-2796,cA[74]+=cA[17]-514,cA[76]+=cA[35]-1978,cA[85]+=cA[8]-2941,cA[95]+=cA[13]- -1104;break;case 43:case cA[73]- -808:cA[8]+=cA[88]-4890,cA[24]+=cA[66]- -2080,cA[28]+=cA[34]- -243,cA[42]+=cA[76]- -641,cA[69]+=cA[34]-1088,cA[74]+=cA[68]- -625,cA[76]+=cA[10]- -1283,cA[85]+=cA[94]-984,cA[95]+=cA[92]-907;break;case-493:case cA[33]-57:case 77:cA[8]+=cA[44]-3966,cA[24]+=cA[33]-662,cA[28]+=cA[83]- -111,cA[42]+=cA[87]- -173,cA[69]+=cA[56]- -140,cA[74]+=cA[86]- -1572,cA[76]+=cA[70]- -207,cA[85]+=cA[41]- -360,cA[95]+=cA[17]- -1659;break;case-906:case-711:case 17:cA[8]+=cA[95]-2515,cA[24]+=cA[41]- -2372,cA[28]+=cA[18]-1645,cA[42]+=cA[63]- -985,cA[69]+=cA[1]- -284,cA[74]+=cA[53]-891,cA[76]+=cA[95]-382,cA[85]+=cA[94]- -2930,cA[95]+=cA[27]- -1479;break}}}var cH;var cE=cA([...cD(0,8),-2819,...cD(9,24),848,...cD(25,28),-668,...cD(29,42),956,...cD(43,69),411,...cD(70,74),-993,237,130,...cD(77,85),601,...cD(86,95),484]);if(cH){return cE}}function gh(cy,cA){return gg(cz["cg"]["j"]["slice"](cy,cy+cA))}var gi=cy["fn"];var gj=this[(1,cz["cg"]["b"])(1563,7)];var gk=this[gh(1574,10)];var gl=cz["cg"]["B"]+gi[gh(1590,10)];var gm=gk+gl;while(gj[gh(1601,8)]88?13:14;do{cE["U"]["d"].push(cE["U"]["e"]&cA[25]+740);cE["U"]["e"]>>=8;cE["U"]["f"]-=cA[79]+692}while(cE["U"]["f"]>cA[83]+-636);cE["U"]["g"]=-1}}if(cE["U"]["g"]>-1){cA[9]+=cA[37]- -789,cA[10]+=cA[29]-1163,cA[11]+=cA[8]- -1842,cA[19]+=cA[5]-1156,cA[27]+=cA[52]-420,cA[44]+=cA[82]-590,cA[50]+=cA[52]-576,cA[61]+=cA[27]- -305,cA[74]+=cA[14]- -2248;break}else{cA[9]+=cA[45]- -1237,cA[10]+=cA[70]-561,cA[11]+=cA[3]-22,cA[19]+=cA[47]-1383,cA[27]+=cA[77]- -3260,cA[44]+=cA[91]-378,cA[50]+=cA[34]-2573,cA[61]+=cA[89]-111,cA[74]+=cA[81]- -1121;break}case cA[92]-211:cE["U"]["a"]=cB(cA[62]+550580,2875,91);cE["U"]["b"]=""+(cy||"");cE["U"]["c"]=cE["U"]["b"].length;if(cA[44]!=780){cA[9]+=cA[27]-1342,cA[10]+=cA[22]-31,cA[11]+=cA[59]-206,cA[19]+=cA[28]- -1116,cA[27]+=cA[47]-1911,cA[44]+=cA[52]-260,cA[50]+=cA[12]-277,cA[61]+=cA[42]- -1637,cA[74]+=cA[62]-2387;break}cA[9]+=cA[77]- -3865,cA[10]+=cA[72]- -46,cA[11]+=cA[3]-496,cA[19]+=cA[34]-1267,cA[27]+=cA[30]-2345,cA[44]+=cA[76]-647,cA[50]+=cA[55]-397,cA[61]+=cA[13]- -491,cA[74]+=cA[82]-1747;break;case-369:case 468:case cA[43]-1184:cE["U"]["d"]=[];cE["U"]["e"]=cA[14]+-105;cA[9]+=cA[38]-3543,cA[10]+=cA[63]- -1941,cA[11]+=cA[31]-689,cA[19]+=cA[66]- -1097,cA[27]+=cA[91]- -550,cA[44]+=cA[51]-992,cA[50]+=cA[88]- -526,cA[61]+=cA[88]- -1547,cA[74]+=cA[15]- -1018;break;case-176:case-221:case 320:cE["U"]["d"].push((cE["U"]["e"]|cE["U"]["g"]<cA[47]+-536){cA[6]+=cA[50]- -1150,cA[12]+=cA[65]- -665,cA[13]+=cA[66]- -1501,cA[15]+=cA[35]- -541,cA[16]+=cA[15]- -173,cA[33]+=cA[62]-2322,cA[38]+=cA[2]- -354,cA[47]+=cA[65]-1052,cA[59]+=cA[7]-17,cA[61]+=cA[64]- -404;break}cA[6]+=cA[8]- -1262,cA[12]+=cA[73]- -715,cA[13]+=cA[54]-2297,cA[15]+=cA[9]-854,cA[16]+=cA[55]- -430,cA[33]+=cA[7]- -761,cA[38]+=cA[72]-165,cA[47]+=cA[10]-312,cA[59]+=cA[76]- -416,cA[61]+=cA[57]- -462;break;case 410:cE["W"]["a"]=cB(cA[63]+598949,3065,91);cE["W"]["b"]=""+(cy||"");cE["W"]["c"]=cE["W"]["b"].length;cA[6]+=cA[0]-170,cA[12]+=cA[38]- -292,cA[13]+=cA[71]- -446,cA[15]+=cA[37]- -867,cA[16]+=cA[0]-1407,cA[33]+=cA[21]-1642,cA[38]+=cA[40]- -847,cA[47]+=cA[34]-1403,cA[59]+=cA[38]-987,cA[61]+=cA[12]-29;break;case-722:case 437:case cA[23]- -930:cA[6]+=cA[76]-1225,cA[12]+=cA[32]-973,cA[13]+=cA[68]-847,cA[15]+=cA[17]- -427,cA[16]+=cA[54]-1032,cA[33]+=cA[18]- -1242,cA[38]+=cA[24]-261,cA[47]+=cA[64]- -739,cA[59]+=cA[74]-255,cA[61]+=cA[17]-536;break;case-99:case 935:case-914:if(!(cA[cA[11]+898]<513)){cA[6]+=cA[79]- -660,cA[12]+=cA[2]- -1267,cA[13]+=cA[75]- -2318,cA[15]+=cA[66]-589,cA[16]+=cA[53]-401,cA[33]+=cA[2]- -2377,cA[38]+=cA[24]-1336,cA[47]+=cA[58]-1233,cA[59]+=cA[24]-2382,cA[61]+=cA[5]- -27;break}if(cA[75]>cA[47]+254){cA[6]+=cA[28]-151,cA[12]+=cA[52]- -473,cA[13]+=cA[18]- -2371,cA[15]+=cA[39]-1875,cA[16]+=cA[43]-1333,cA[33]+=cA[28]- -1358,cA[38]+=cA[12]-182,cA[47]+=cA[76]-961,cA[59]+=cA[3]-2172,cA[61]+=cA[49]- -739;break}cA[6]+=cA[49]-46,cA[12]+=cA[69]-288,cA[13]+=cA[29]- -193,cA[15]+=cA[62]-2128,cA[16]+=cA[29]- -219,cA[33]+=cA[32]- -2147,cA[38]+=cA[19]-76,cA[47]+=cA[22]-140,cA[59]+=cA[26]-1078,cA[61]+=cA[17]-452;break;case 956:case cA[24]-346:return cH=true,(1,cz["cg"]["d"])(cE["W"]["d"]);cA[6]+=cA[1]- -1315,cA[12]+=cA[46]-10,cA[13]+=cA[76]-1445,cA[15]+=cA[3]-820,cA[16]+=cA[8]- -469,cA[33]+=cA[19]-335,cA[38]+=cA[6]-345,cA[47]+=cA[26]- -107,cA[59]+=cA[61]- -609,cA[61]+=cA[1]- -444;break;case cA[44]-580:case-13:case 824:cE["W"]["d"]=[];cE["W"]["e"]=cA[4]+665;cA[6]+=cA[64]-1277,cA[12]+=cA[49]-496,cA[13]+=cA[67]- -909,cA[15]+=cA[23]- -1239,cA[16]+=cA[60]-1814,cA[33]+=cA[16]- -928,cA[38]+=cA[49]- -563,cA[47]+=cA[67]- -606,cA[59]+=cA[61]-984,cA[61]+=cA[65]- -1539;break;case 325:cA[6]+=cA[23]- -565,cA[12]+=cA[60]- -71,cA[13]+=cA[14]- -57,cA[15]+=cA[14]-798,cA[16]+=cA[38]-267,cA[33]+=cA[19]-800,cA[38]+=cA[60]-886,cA[47]+=cA[53]-707,cA[59]+=cA[69]-1210,cA[61]+=cA[73]- -1520;break;case 733:cA[6]+=cA[60]-1923,cA[12]+=cA[45]-763,cA[13]+=cA[17]- -1072,cA[15]+=cA[70]- -645,cA[16]+=cA[11]- -1128,cA[33]+=cA[6]- -824,cA[38]+=cA[33]- -1038,cA[47]+=cA[74]-2087,cA[59]+=cA[1]- -883,cA[61]+=cA[33]- -729;break;case cA[6]- -1110:case-253:cA[6]+=cA[27]- -597,cA[12]+=cA[75]-1293,cA[13]+=cA[28]- -1018,cA[15]+=cA[74]- -264,cA[16]+=cA[1]- -1074,cA[33]+=cA[75]-589,cA[38]+=cA[63]- -2612,cA[47]+=cA[24]-3563,cA[59]+=cA[18]-150,cA[61]+=cA[59]- -183;break;case 644:case cA[31]-40:cE["W"]["f"]=cA[38]+-900;cE["W"]["g"]=-1;for(cE["W"]["h"]=0;cE["W"]["h"]88?13:cA[30]+-831;do{cE["W"]["d"].push(cE["W"]["e"]&cA[32]+-661);cE["W"]["e"]>>=8;cE["W"]["f"]-=8}while(cE["W"]["f"]>7);cE["W"]["g"]=-(cA[6]+852)}}if(cE["W"]["g"]>-(cA[68]+256)){cA[6]+=cA[25]- -1026,cA[12]+=cA[58]-742,cA[13]+=cA[76]-1041,cA[15]+=cA[20]-1019,cA[16]+=cA[0]- -4296,cA[33]+=cA[56]- -723,cA[38]+=cA[42]-670,cA[47]+=cA[71]- -489,cA[59]+=cA[4]- -637,cA[61]+=cA[56]-334;break}else{cA[6]+=cA[65]- -655,cA[12]+=cA[70]-510,cA[13]+=cA[63]- -1805,cA[15]+=cA[75]- -547,cA[16]+=cA[13]-301,cA[33]+=cA[59]- -1006,cA[38]+=cA[16]- -44,cA[47]+=cA[48]-1586,cA[59]+=cA[41]- -1212,cA[61]+=cA[56]- -523;break}}}}var cH;var cE=cA([...cD(0,6),-981,...cD(7,12),-434,-739,105,-289,653,...cD(17,33),876,...cD(34,38),706,...cD(39,47),473,...cD(48,59),450,982,-627,...cD(62,80)]);if(cH){return cE}}function gs(cy,cA){return gr(cz["cg"]["j"]["slice"](cy,cy+cA))}gj[gk+cz["cg"]["A"][gs(1763,15)]]=gi[gs(1784,13)]}if(typeof cz["cg"]["A"][gh(1800,13)]===gh(1736,8)){function gt(cy){function cA(cA,cE={["Y"]:{}},gg){while(cC(cA)!==834){switch(cC(cA)){case-335:case 560:case 460:if(!(cA[cA[41]+830]==cA[79]+1675)){cA[0]+=cA[77]- -4853,cA[1]+=cA[37]- -1429,cA[17]+=cA[66]-4421,cA[29]+=cA[81]- -373,cA[39]+=cA[10]- -1041,cA[60]+=cA[54]-946,cA[69]+=cA[67]-233,cA[74]+=cA[77]- -320,cA[87]+=cA[44]-332;break}cA[0]+=cA[38]-494,cA[1]+=cA[63]- -173,cA[17]+=cA[47]-4677,cA[29]+=cA[73]- -325,cA[39]+=cA[36]- -795,cA[60]+=cA[77]- -4896,cA[69]+=cA[25]- -832,cA[74]+=cA[94]- -310,cA[87]+=cA[43]-834;break;case-795:case 939:cA[0]+=cA[22]-2483,cA[1]+=cA[68]-1227,cA[17]+=cA[25]- -376,cA[29]+=cA[1]-1118,cA[39]+=cA[17]- -541,cA[60]+=cA[32]- -2813,cA[69]+=cA[9]-730,cA[74]+=cA[53]- -802,cA[87]+=cA[13]- -49;break;case-326:case-620:cA[0]+=cA[33]-567,cA[1]+=cA[5]-2387,cA[17]+=cA[13]- -1022,cA[29]+=cA[9]- -350,cA[39]+=cA[62]-532,cA[60]+=cA[21]- -67,cA[69]+=cA[10]- -1006,cA[74]+=cA[53]-1369,cA[87]+=cA[6]-308;break;case cA[50]- -716:cA[0]+=cA[60]-1893,cA[1]+=cA[56]- -2728,cA[17]+=cA[0]-1087,cA[29]+=cA[81]- -1233,cA[39]+=cA[54]-314,cA[60]+=cA[72]-464,cA[69]+=cA[49]-131,cA[74]+=cA[52]-894,cA[87]+=cA[39]-339;break;case cA[55]- -216:cA[0]+=cA[64]-1194,cA[1]+=cA[92]-690,cA[17]+=cA[93]- -856,cA[29]+=cA[42]-140,cA[39]+=cA[35]-955,cA[60]+=cA[58]- -2760,cA[69]+=cA[26]-1132,cA[74]+=cA[63]- -1534,cA[87]+=cA[41]-482;break;case cA[34]-293:cE["Y"]["d"]=[];cE["Y"]["e"]=0;if(cA[cA[75]+-181]>-(cA[22]+281)){cA[0]+=cA[70]-3610,cA[1]+=cA[15]- -257,cA[17]+=cA[32]- -355,cA[29]+=cA[3]- -760,cA[39]+=cA[50]- -560,cA[60]+=cA[33]-2019,cA[69]+=cA[6]-258,cA[74]+=cA[77]- -2033,cA[87]+=cA[8]- -1252;break}cA[0]+=cA[78]-1950,cA[1]+=cA[92]- -513,cA[17]+=cA[47]- -133,cA[29]+=cA[24]- -320,cA[39]+=cA[28]-561,cA[60]+=cA[72]-636,cA[69]+=cA[21]-854,cA[74]+=cA[68]- -260,cA[87]+=cA[68]- -88;break;case cA[62]-663:cA[0]+=cA[8]-451,cA[1]+=cA[12]- -39,cA[17]+=cA[87]-0,cA[29]+=cA[34]- -569,cA[39]+=cA[37]- -890,cA[60]+=cA[48]-458,cA[69]+=cA[15]- -147,cA[74]+=cA[19]-535,cA[87]+=cA[48]- -323;break;case 769:case-832:cA[0]+=cA[72]- -3805,cA[1]+=cA[73]- -1735,cA[17]+=cA[62]-2295,cA[29]+=cA[22]-521,cA[39]+=cA[86]- -879,cA[60]+=cA[22]-3263,cA[69]+=cA[72]-1443,cA[74]+=cA[35]- -60,cA[87]+=cA[26]-407;break;case cA[16]- -1346:cE["Y"]["a"]=cB(cA[47]+974341,3259,91);cE["Y"]["b"]=""+(cy||"");cE["Y"]["c"]=cE["Y"]["b"].length;if(cA[cA[32]+-911]==cA[15]+833){cA[0]+=cA[16]- -1306,cA[1]+=cA[2]- -1644,cA[17]+=cA[78]- -4278,cA[29]+=cA[54]-492,cA[39]+=cA[64]-2593,cA[60]+=cA[50]-4037,cA[69]+=cA[42]- -532,cA[74]+=cA[41]-214,cA[87]+=cA[18]-196;break}cA[0]+=cA[30]- -3231,cA[1]+=cA[51]-843,cA[17]+=cA[39]-1335,cA[29]+=cA[5]- -22,cA[39]+=cA[26]-1260,cA[60]+=cA[44]-3292,cA[69]+=cA[84]- -428,cA[74]+=cA[7]-1131,cA[87]+=cA[0]-2835;break;case cA[66]- -377:cE["Y"]["f"]=0;cE["Y"]["g"]=-(cA[53]+87);for(cE["Y"]["h"]=cA[22]+-630;cE["Y"]["h"]cA[40]+923?13:14;do{cE["Y"]["d"].push(cE["Y"]["e"]&cA[30]+-590);cE["Y"]["e"]>>=8;cE["Y"]["f"]-=cA[91]+550}while(cE["Y"]["f"]>cA[76]+-84);cE["Y"]["g"]=-1}}if(cE["Y"]["g"]>-1){cA[0]+=cA[69]- -2409,cA[1]+=cA[87]- -131,cA[17]+=cA[11]- -32,cA[29]+=cA[63]-247,cA[39]+=cA[17]- -1172,cA[60]+=cA[43]-1243,cA[69]+=cA[79]-516,cA[74]+=cA[87]- -484,cA[87]+=cA[70]-528;break}else{cA[0]+=cA[63]- -279,cA[1]+=cA[82]-1575,cA[17]+=cA[43]-479,cA[29]+=cA[2]- -628,cA[39]+=cA[31]- -716,cA[60]+=cA[84]-405,cA[69]+=cA[90]- -904,cA[74]+=cA[20]- -778,cA[87]+=cA[66]- -470;break}case-587:cE["Y"]["d"].push((cE["Y"]["e"]|cE["Y"]["g"]<cA[36]+49){cA[0]+=cA[69]-1159,cA[1]+=cA[9]-575,cA[17]+=cA[21]-200,cA[29]+=cA[38]-818,cA[39]+=cA[53]-502,cA[60]+=cA[71]- -2542,cA[69]+=cA[74]-605,cA[74]+=cA[57]-207,cA[87]+=cA[84]-1151;break}cA[0]+=cA[28]-719,cA[1]+=cA[36]-703,cA[17]+=cA[14]- -141,cA[29]+=cA[88]- -474,cA[39]+=cA[76]- -502,cA[60]+=cA[54]-1793,cA[69]+=cA[21]-592,cA[74]+=cA[30]-941,cA[87]+=cA[19]- -877;break}}}var cH;var cE=cA([-984,-525,...cD(2,17),-57,...cD(18,29),-879,...cD(30,39),808,...cD(40,60),3886,...cD(61,69),-340,...cD(70,74),893,...cD(75,87),-355,...cD(88,95)]);if(cH){return cE}}function gu(cy,cA){return gt(cz["cg"]["j"]["slice"](cy,cy+cA))}gj[gk+cz["cg"]["A"][gu(1817,13)]]=cA}if(typeof cz["cg"]["A"][gh(1830,10)]===gh(1736,8)){function gv(cy){function cA(cA,cE={["aa"]:{}},gg){while(cC(cA)!==852){switch(cC(cA)){case-182:if(cA[cA[50]+38]<611){cA[4]+=cA[67]- -85,cA[14]+=cA[87]- -661,cA[43]+=cA[75]- -4013,cA[61]+=cA[77]- -390,cA[72]+=cA[24]-584,cA[73]+=cA[59]-529,cA[79]+=cA[33]-6007,cA[82]+=cA[86]-194,cA[91]+=cA[31]- -167,cA[93]+=cA[40]- -1975,cA[95]+=cA[51]-1206;break}cA[4]+=cA[47]-2581,cA[14]+=cA[91]-134,cA[43]+=cA[86]- -3570,cA[61]+=cA[65]-703,cA[72]+=cA[38]-757,cA[73]+=cA[89]- -858,cA[79]+=cA[69]-241,cA[82]+=cA[63]- -19,cA[91]+=cA[8]- -1968,cA[93]+=cA[36]- -538,cA[95]+=cA[40]- -903;break;case 809:case 681:cE["aa"]["g"]=-(cA[13]+340);for(cE["aa"]["h"]=cA[18]+-184;cE["aa"]["h"]cA[33]+-656?cA[51]+-978:14;do{cE["aa"]["d"].push(cE["aa"]["e"]&255);cE["aa"]["e"]>>=cA[35]+187;cE["aa"]["f"]-=8}while(cE["aa"]["f"]>cA[0]+-649);cE["aa"]["g"]=-1}}if(cE["aa"]["g"]>-(cA[19]+61)){cA[4]+=cA[62]-848,cA[14]+=cA[82]-166,cA[43]+=cA[3]-1153,cA[61]+=cA[16]- -568,cA[72]+=cA[51]- -1916,cA[73]+=cA[4]- -265,cA[79]+=cA[92]-1913,cA[82]+=cA[36]-1214,cA[91]+=cA[55]-1731,cA[93]+=cA[13]- -1625,cA[95]+=cA[71]-561;break}else{cA[4]+=cA[36]-4114,cA[14]+=cA[31]- -351,cA[43]+=cA[7]-318,cA[61]+=cA[27]-192,cA[72]+=cA[22]- -2713,cA[73]+=cA[79]- -478,cA[79]+=cA[93]- -306,cA[82]+=cA[42]- -609,cA[91]+=cA[30]-946,cA[93]+=cA[30]- -478,cA[95]+=cA[92]-753;break}if(cA[cA[3]+-568]<-719){cA[4]+=cA[25]-597,cA[14]+=cA[61]- -690,cA[43]+=cA[2]- -874,cA[61]+=cA[55]-301,cA[72]+=cA[44]- -4312,cA[73]+=cA[92]- -109,cA[79]+=cA[65]-5540,cA[82]+=cA[46]- -316,cA[91]+=cA[14]-2699,cA[93]+=cA[37]- -1946,cA[95]+=cA[70]-576;break}case-86:case-552:if(cA[cA[38]+264]>-(cA[41]+1387)){cA[4]+=cA[20]- -59,cA[14]+=cA[60]-2404,cA[43]+=cA[34]-2191,cA[61]+=cA[58]- -1112,cA[72]+=cA[83]- -781,cA[73]+=cA[64]-1686,cA[79]+=cA[53]- -32,cA[82]+=cA[0]-1654,cA[91]+=cA[18]- -787,cA[93]+=cA[16]- -1412,cA[95]+=cA[59]-236;break}cA[4]+=cA[94]- -260,cA[14]+=cA[78]- -649,cA[43]+=cA[54]-2332,cA[61]+=cA[74]- -1423,cA[72]+=cA[48]-644,cA[73]+=cA[89]-1981,cA[79]+=cA[11]- -360,cA[82]+=cA[50]-377,cA[91]+=cA[84]- -390,cA[93]+=cA[56]- -298,cA[95]+=cA[54]-473;break;case cA[88]-594:if(cA[22]<611){cA[4]+=cA[90]-473,cA[14]+=cA[81]- -1244,cA[43]+=cA[23]- -2179,cA[61]+=cA[90]- -429,cA[72]+=cA[54]- -278,cA[73]+=cA[53]- -2576,cA[79]+=cA[2]-3975,cA[82]+=cA[35]-362,cA[91]+=cA[39]-2081,cA[93]+=cA[47]- -362,cA[95]+=cA[3]- -109;break}cA[4]+=cA[61]-3520,cA[14]+=cA[92]-972,cA[43]+=cA[88]- -505,cA[61]+=cA[22]-1582,cA[72]+=cA[57]- -747,cA[73]+=cA[56]- -3884,cA[79]+=cA[91]- -288,cA[82]+=cA[22]-1654,cA[91]+=cA[0]-249,cA[93]+=cA[64]- -241,cA[95]+=cA[93]- -506;break;case cA[85]- -1542:return cH=true,(1,cz["cg"]["d"])(cE["aa"]["d"]);cA[4]+=cA[3]- -2181,cA[14]+=cA[4]-1958,cA[43]+=cA[51]-1353,cA[61]+=cA[42]- -1002,cA[72]+=cA[63]- -2701,cA[73]+=cA[63]-375,cA[79]+=cA[27]- -374,cA[82]+=cA[34]-507,cA[91]+=cA[77]- -390,cA[93]+=cA[52]- -348,cA[95]+=cA[91]-1606;break;case cA[78]- -1116:cE["aa"]["d"].push((cE["aa"]["e"]|cE["aa"]["g"]<cA[20]+-415?cA[84]+-614:14;do{cE["ac"]["d"].push(cE["ac"]["e"]&cA[51]+-736);cE["ac"]["e"]>>=8;cE["ac"]["f"]-=cA[45]+634}while(cE["ac"]["f"]>cA[4]+672);cE["ac"]["g"]=-1}}if(cE["ac"]["g"]>-(cA[63]+926)){cA[8]+=cA[91]- -2170,cA[9]+=cA[5]-986,cA[23]+=cA[30]-3864,cA[53]+=cA[10]-636,cA[61]+=cA[33]- -1017,cA[67]+=cA[27]- -1273,cA[70]+=cA[94]-162,cA[77]+=cA[52]- -1303;break}else{cA[8]+=cA[2]- -1911,cA[9]+=cA[11]-3371,cA[23]+=cA[78]-289,cA[53]+=cA[30]- -316,cA[61]+=cA[92]- -208,cA[67]+=cA[21]-357,cA[70]+=cA[31]- -1500,cA[77]+=cA[83]-65;break}case 69:cA[8]+=cA[9]-1296,cA[9]+=cA[22]-1380,cA[23]+=cA[77]-3108,cA[53]+=cA[76]- -661,cA[61]+=cA[66]-312,cA[67]+=cA[21]- -3735,cA[70]+=cA[12]-1103,cA[77]+=cA[4]- -1536;break;case cA[4]- -1509:if(!(cA[cA[92]+-716]cA[9]+834){cA[8]+=cA[67]-2256,cA[9]+=cA[18]-19,cA[23]+=cA[57]- -4286,cA[53]+=cA[46]- -379,cA[61]+=cA[63]-217,cA[67]+=cA[52]- -408,cA[70]+=cA[41]- -1529,cA[77]+=cA[8]-275;break}cA[8]+=cA[63]- -316,cA[9]+=cA[54]-4451,cA[23]+=cA[84]- -1977,cA[53]+=cA[19]- -1845,cA[61]+=cA[48]-1078,cA[67]+=cA[59]-948,cA[70]+=cA[75]- -411,cA[77]+=cA[86]-398;break;case-332:case 658:cA[8]+=cA[59]-1638,cA[9]+=cA[77]- -703,cA[23]+=cA[64]-500,cA[53]+=cA[39]-475,cA[61]+=cA[71]-292,cA[67]+=cA[34]-240,cA[70]+=cA[85]- -1206,cA[77]+=cA[15]- -1604;break;case-54:case cA[4]-201:cE["ac"]["e"]=0;cE["ac"]["f"]=cA[11]+831;if(!(cA[cA[16]+682]!=cA[56]+1597)){;break}cA[8]+=cA[26]-620,cA[9]+=cA[81]- -1724,cA[23]+=cA[48]- -2369,cA[53]+=cA[56]-177,cA[61]+=cA[78]-1026,cA[67]+=cA[27]- -761,cA[70]+=cA[69]-1223,cA[77]+=cA[48]-1179;break;case-978:case-788:case-331:return cH=true,(1,cz["cg"]["d"])(cE["ac"]["d"]);cA[8]+=cA[2]-74,cA[9]+=cA[6]- -2549,cA[23]+=cA[84]-446,cA[53]+=cA[48]-1352,cA[61]+=cA[70]-775,cA[67]+=cA[78]- -506,cA[70]+=cA[79]- -598,cA[77]+=cA[87]-115;break}}}var cH;var cE=cA([...cD(0,8),-450,-770,...cD(10,23),907,...cD(24,53),372,...cD(54,61),-506,...cD(62,67),121,...cD(68,70),-856,...cD(71,77),-246,...cD(78,95)]);if(cH){return cE}}function gy(cy,cA){return gx(cz["cg"]["j"]["slice"](cy,cy+cA))}if(gi[gh(1888,9)]){function gz(cy){function cA(cA,cE={["ae"]:{}},gg){while(cC(cA)!==484){switch(cC(cA)){case 456:case 785:cA[9]+=cA[83]-520,cA[15]+=cA[47]-4038,cA[26]+=cA[83]- -1989,cA[32]+=cA[27]- -1720,cA[40]+=cA[56]- -231,cA[43]+=cA[77]- -738,cA[46]+=cA[33]-455,cA[56]+=cA[67]- -184,cA[78]+=cA[53]-400,cA[88]+=cA[26]-4001,cA[93]+=cA[52]- -231;break;case cA[73]- -1101:cE["ae"]["d"].push((cE["ae"]["e"]|cE["ae"]["g"]<cA[35]+267?cA[12]+-389:cA[64]+-907;do{cE["ae"]["d"].push(cE["ae"]["e"]&cA[89]+712);cE["ae"]["e"]>>=8;cE["ae"]["f"]-=cA[56]+313}while(cE["ae"]["f"]>cA[76]+-84);cE["ae"]["g"]=-1}}if(cE["ae"]["g"]>-1){cA[9]+=cA[54]-481,cA[15]+=cA[56]- -132,cA[26]+=cA[34]-62,cA[32]+=cA[30]-3060,cA[40]+=cA[12]-928,cA[43]+=cA[92]-1954,cA[46]+=cA[40]- -790,cA[56]+=cA[81]- -1233,cA[78]+=cA[85]- -1773,cA[88]+=cA[75]- -263,cA[93]+=cA[87]-161;break}else{cA[9]+=cA[25]- -553,cA[15]+=cA[45]-2301,cA[26]+=cA[25]- -1527,cA[32]+=cA[53]-2032,cA[40]+=cA[88]- -333,cA[43]+=cA[5]-2104,cA[46]+=cA[79]- -1914,cA[56]+=cA[20]- -648,cA[78]+=cA[51]- -314,cA[88]+=cA[81]- -1409,cA[93]+=cA[30]-241;break}case-354:case-99:case 676:cE["ae"]["d"].push((cE["ae"]["e"]|cE["ae"]["g"]<343){cA[10]+=cA[73]-3084,cA[21]+=cA[73]-3481,cA[34]+=cA[47]-1231,cA[46]+=cA[43]- -46,cA[62]+=cA[72]- -1051,cA[64]+=cA[14]-49,cA[73]+=cA[41]-3374,cA[75]+=cA[35]- -2936,cA[88]+=cA[1]- -461;break}cA[10]+=cA[80]-962,cA[21]+=cA[59]-218,cA[34]+=cA[62]- -6423,cA[46]+=cA[77]- -581,cA[62]+=cA[20]-667,cA[64]+=cA[91]- -549,cA[73]+=cA[72]-4400,cA[75]+=cA[83]-132,cA[88]+=cA[66]-580;break;case-996:cE["ag"]["g"]=-(cA[67]+-13);for(cE["ag"]["h"]=cA[42]+879;cE["ag"]["h"]88?cA[30]+-832:14;do{cE["ag"]["d"].push(cE["ag"]["e"]&cA[52]+353);cE["ag"]["e"]>>=8;cE["ag"]["f"]-=cA[55]+-263}while(cE["ag"]["f"]>7);cE["ag"]["g"]=-1}}if(cE["ag"]["g"]>-(cA[29]+134)){cA[10]+=cA[29]- -826,cA[21]+=cA[8]-302,cA[34]+=cA[28]-471,cA[46]+=cA[8]- -1739,cA[62]+=cA[73]- -185,cA[64]+=cA[59]-2332,cA[73]+=cA[65]-270,cA[75]+=cA[90]- -2805,cA[88]+=cA[47]-827;break}else{cA[10]+=cA[2]- -678,cA[21]+=cA[32]-2238,cA[34]+=cA[79]- -609,cA[46]+=cA[16]- -1667,cA[62]+=cA[10]- -512,cA[64]+=cA[35]- -10,cA[73]+=cA[56]- -1820,cA[75]+=cA[28]- -902,cA[88]+=cA[47]-1205;break}case-874:case 709:cA[10]+=cA[8]- -6458,cA[21]+=cA[42]- -423,cA[34]+=cA[7]-134,cA[46]+=cA[68]-256,cA[62]+=cA[70]-412,cA[64]+=cA[8]- -148,cA[73]+=cA[22]-1134,cA[75]+=cA[57]-1449,cA[88]+=cA[71]- -681;break;case 931:if(cA[cA[24]+-717]<-454){cA[10]+=cA[18]-1232,cA[21]+=cA[11]- -560,cA[34]+=cA[24]- -6012,cA[46]+=cA[53]-1068,cA[62]+=cA[79]-559,cA[64]+=cA[47]-574,cA[73]+=cA[21]- -155,cA[75]+=cA[73]-1855,cA[88]+=cA[22]-1583;break}cA[10]+=cA[29]-889,cA[21]+=cA[19]-281,cA[34]+=cA[38]- -1175,cA[46]+=cA[20]-1085,cA[62]+=cA[82]-281,cA[64]+=cA[33]- -212,cA[73]+=cA[38]- -1310,cA[75]+=cA[70]-2257,cA[88]+=cA[60]-1278;break;case-911:case-463:return cH=true,(1,cz["cg"]["d"])(cE["ag"]["d"]);cA[10]+=cA[16]- -285,cA[21]+=cA[6]-218,cA[34]+=cA[49]- -484,cA[46]+=cA[41]- -1576,cA[62]+=cA[26]-647,cA[64]+=cA[16]-197,cA[73]+=cA[50]- -1547,cA[75]+=cA[76]-441,cA[88]+=cA[19]-172;break;case-977:case cA[12]- -560:cE["ag"]["e"]=0;cE["ag"]["f"]=0;cA[10]+=cA[62]-498,cA[21]+=cA[89]- -1978,cA[34]+=cA[28]- -1759,cA[46]+=cA[35]-5370,cA[62]+=cA[66]- -1679,cA[64]+=cA[36]- -251,cA[73]+=cA[31]-3,cA[75]+=cA[90]-467,cA[88]+=cA[31]- -684;break;case-840:cE["ag"]["d"].push((cE["ag"]["e"]|cE["ag"]["g"]<cA[91]+630?13:14;do{cE["ai"]["d"].push(cE["ai"]["e"]&255);cE["ai"]["e"]>>=cA[3]+-581;cE["ai"]["f"]-=8}while(cE["ai"]["f"]>7);cE["ai"]["g"]=-(cA[50]+17)}}if(cE["ai"]["g"]>-(cA[75]+-236)){cA[12]+=cA[25]- -366,cA[30]+=cA[63]- -1248,cA[48]+=cA[90]- -994,cA[53]+=cA[41]-146,cA[59]+=cA[92]-1617,cA[66]+=cA[0]-1169,cA[73]+=cA[91]- -2120,cA[94]+=cA[41]- -1392;break}else{cA[12]+=cA[7]-156,cA[30]+=cA[29]-831,cA[48]+=cA[52]- -427,cA[53]+=cA[82]-416,cA[59]+=cA[73]- -4,cA[66]+=cA[22]-1245,cA[73]+=cA[1]- -1654,cA[94]+=cA[37]- -2362;break}case cA[37]- -1052:cE["ai"]["c"]=cE["ai"]["b"].length;cE["ai"]["d"]=[];if(cA[36]<-769){cA[12]+=cA[70]-334,cA[30]+=cA[75]-1786,cA[48]+=cA[12]-1764,cA[53]+=cA[93]- -2872,cA[59]+=cA[43]- -33,cA[66]+=cA[49]- -283,cA[73]+=cA[47]- -140,cA[94]+=cA[3]-985;break}if(cA[37]>596){cA[12]+=cA[86]- -736,cA[30]+=cA[13]- -67,cA[48]+=cA[93]-1265,cA[53]+=cA[71]- -2129,cA[59]+=cA[37]- -1683,cA[66]+=cA[52]-1311,cA[73]+=cA[7]- -1257,cA[94]+=cA[34]-1672;break}cA[12]+=cA[2]- -1918,cA[30]+=cA[20]-1118,cA[48]+=cA[28]-1393,cA[53]+=cA[6]- -491,cA[59]+=cA[42]- -2361,cA[66]+=cA[50]-480,cA[73]+=cA[62]-763,cA[94]+=cA[80]-2036;break;case cA[0]-1513:return cH=true,(1,cz["cg"]["d"])(cE["ai"]["d"]);cA[12]+=cA[68]-736,cA[30]+=cA[83]-130,cA[48]+=cA[7]- -1062,cA[53]+=cA[84]-809,cA[59]+=cA[53]- -1328,cA[66]+=cA[75]-567,cA[73]+=cA[33]-544,cA[94]+=cA[29]-583;break;case 310:cE["ai"]["d"].push((cE["ai"]["e"]|cE["ai"]["g"]<-677)){cA[12]+=cA[16]- -516,cA[30]+=cA[75]- -197,cA[48]+=cA[14]- -1326,cA[53]+=cA[45]- -257,cA[59]+=cA[51]-1404,cA[66]+=cA[2]- -1444,cA[73]+=cA[63]- -729,cA[94]+=cA[66]-191;break}cA[12]+=cA[15]-1391,cA[30]+=cA[4]- -325,cA[48]+=cA[71]- -2270,cA[53]+=cA[73]-920,cA[59]+=cA[14]- -639,cA[66]+=cA[63]- -1045,cA[73]+=cA[66]- -300,cA[94]+=cA[65]- -277;break;case 226:case 53:case-573:}}}var cH;var cE=cA([...cD(0,12),290,...cD(13,30),-673,...cD(31,48),1508,...cD(49,53),112,...cD(54,59),-805,...cD(60,66),897,...cD(67,73),444,...cD(74,94),-960]);if(cH){return cE}}function gH(cy,cA){return gG(cz["cg"]["j"]["slice"](cy,cy+cA))}gj[gq+gi[gH(1999,13)]]=cA}}this["_f"]=gk};cz["cg"]["i"][(1,cz["cg"]["b"])(cy[54]+1434,cy[61]+621)][(1,cz["cg"]["b"])(cy[47]+1492,cy[76]+-826)]=function(cy,cA){function cH(cy){function cA(cA,cE={["ak"]:{}},gI){while(cC(cA)!==-849){switch(cC(cA)){case cA[20]- -245:case 640:case-948:cA[5]+=cA[12]- -1157,cA[37]+=cA[33]-1440,cA[55]+=cA[38]- -607,cA[61]+=cA[14]-1312,cA[62]+=cA[67]- -897,cA[63]+=cA[54]-398,cA[70]+=cA[16]-758,cA[77]+=cA[42]- -383;break;case 239:cE["ak"]["a"]=cB(cA[66]+764831,4403,91);cE["ak"]["b"]=""+(cy||"");cE["ak"]["c"]=cE["ak"]["b"].length;cA[5]+=cA[83]-3824,cA[37]+=cA[26]-1168,cA[55]+=cA[79]- -1203,cA[61]+=cA[34]- -1089,cA[62]+=cA[5]- -2575,cA[63]+=cA[34]-1126,cA[70]+=cA[59]- -435,cA[77]+=cA[45]- -796;break;case-764:case-604:cE["ak"]["d"]=[];cE["ak"]["e"]=0;cA[5]+=cA[58]- -1741,cA[37]+=cA[47]-216,cA[55]+=cA[75]-366,cA[61]+=cA[27]-225,cA[62]+=cA[53]- -1134,cA[63]+=cA[3]- -894,cA[70]+=cA[80]-3112,cA[77]+=cA[79]- -57;break;case cA[56]- -1814:for(cE["ak"]["h"]=cA[78]+126;cE["ak"]["h"]cA[21]+-345?13:14;do{cE["ak"]["d"].push(cE["ak"]["e"]&cA[85]+916);cE["ak"]["e"]>>=cA[47]+-517;cE["ak"]["f"]-=cA[68]+263}while(cE["ak"]["f"]>7);cE["ak"]["g"]=-1}}if(cE["ak"]["g"]>-1){cA[5]+=cA[14]- -167,cA[37]+=cA[17]- -120,cA[55]+=cA[48]-204,cA[61]+=cA[35]- -590,cA[62]+=cA[0]- -917,cA[63]+=cA[56]-1453,cA[70]+=cA[42]- -1461,cA[77]+=cA[21]-582;break}else{cA[5]+=cA[12]-3496,cA[37]+=cA[72]-301,cA[55]+=cA[77]- -2144,cA[61]+=cA[45]-162,cA[62]+=cA[7]- -2365,cA[63]+=cA[78]- -411,cA[70]+=cA[31]-593,cA[77]+=cA[75]-430;break}case 444:case-50:case cA[83]-940:cA[5]+=cA[78]- -6,cA[37]+=cA[7]- -4035,cA[55]+=cA[8]- -8,cA[61]+=cA[82]- -1182,cA[62]+=cA[84]-738,cA[63]+=cA[52]-2736,cA[70]+=cA[49]- -432,cA[77]+=cA[49]- -197;break;case-762:return cH=true,(1,cz["cg"]["d"])(cE["ak"]["d"]);cA[5]+=cA[24]- -2943,cA[37]+=cA[55]-1381,cA[55]+=cA[43]-573,cA[61]+=cA[28]-4400,cA[62]+=cA[57]- -373,cA[63]+=cA[3]-707,cA[70]+=cA[2]- -2378,cA[77]+=cA[85]- -662;break;case cA[77]- -2475:case 506:case 412:cA[5]+=cA[25]- -1301,cA[37]+=cA[17]-2118,cA[55]+=cA[74]-1211,cA[61]+=cA[81]-1296,cA[62]+=cA[11]- -2595,cA[63]+=cA[12]-3,cA[70]+=cA[26]-818,cA[77]+=cA[57]- -2738;break;case cA[1]- -1123:cE["ak"]["d"].push((cE["ak"]["e"]|cE["ak"]["g"]<>>cz["cq"]["a"][35]+179}return cz["cp"]["c"]=cy[6]!=858,cz["cq"]["b"]["u"]["d"];if(cz["cq"]["a"][cz["cq"]["a"][72]+-17]>248){cz["cq"]["a"][6]+=cz["cq"]["a"][cy[59]+-444]- -555,cz["cq"]["a"][cy[29]+157]+=cz["cq"]["a"][cy[47]+-468]-(cy[63]+1172),cz["cq"]["a"][cy[7]+635]+=cz["cq"]["a"][68]- -(cy[22]+62),cz["cq"]["a"][30]+=cz["cq"]["a"][cy[52]+137]- -30,cz["cq"]["a"][31]+=cz["cq"]["a"][cy[9]+-928]-(cy[41]+2136),cz["cq"]["a"][cy[49]+425]+=cz["cq"]["a"][cy[9]+-972]- -2521,cz["cq"]["a"][cy[30]+-796]+=cz["cq"]["a"][cy[19]+81]-(cy[19]+464),cz["cq"]["a"][cy[2]+38]+=cz["cq"]["a"][16]-(cy[64]+-821),cz["cq"]["a"][cy[74]+-812]+=cz["cq"]["a"][73]-496;break}cz["cq"]["a"][cy[33]+-738]+=cz["cq"]["a"][cy[63]+257]- -(cy[52]+1135),cz["cq"]["a"][24]+=cz["cq"]["a"][2]-327,cz["cq"]["a"][26]+=cz["cq"]["a"][cy[67]+205]- -(cy[19]+591),cz["cq"]["a"][30]+=cz["cq"]["a"][21]- -(cy[6]+1304),cz["cq"]["a"][31]+=cz["cq"]["a"][59]-1063,cz["cq"]["a"][44]+=cz["cq"]["a"][cy[18]+-111]- -866,cz["cq"]["a"][cy[3]+-540]+=cz["cq"]["a"][cy[21]+-375]-857,cz["cq"]["a"][65]+=cz["cq"]["a"][62]-(cy[27]+2096),cz["cq"]["a"][69]+=cz["cq"]["a"][cy[3]+-527]-(cy[62]+1362);break;case 712:case-736:for(cz["cq"]["b"]["u"]["e"]=cy[75]+-237;cz["cq"]["b"]["u"]["e"]>>cy[51]+-991}return cz["cp"]["c"]=cy[24]!=449,cz["cq"]["b"]["u"]["d"];if(cz["cq"]["a"][11]>cz["cq"]["a"][5]+-83){cz["cq"]["a"][cy[3]+-583]+=cz["cq"]["a"][cy[28]+-832]-569,cz["cq"]["a"][cy[3]+-565]+=cz["cq"]["a"][cy[49]+409]- -778,cz["cq"]["a"][cy[25]+511]+=cz["cq"]["a"][1]- -635,cz["cq"]["a"][cy[62]+-866]+=cz["cq"]["a"][cy[46]+-441]- -894,cz["cq"]["a"][31]+=cz["cq"]["a"][cy[21]+-374]-(cy[75]+1916),cz["cq"]["a"][cy[2]+17]+=cz["cq"]["a"][cy[52]+136]- -2768,cz["cq"]["a"][49]+=cz["cq"]["a"][60]-(cy[69]+-278),cz["cq"]["a"][65]+=cz["cq"]["a"][cy[51]+-945]- -147,cz["cq"]["a"][cy[23]+814]+=cz["cq"]["a"][56]-(cy[14]+-5);break}cz["cq"]["a"][6]+=cz["cq"]["a"][43]-1513,cz["cq"]["a"][24]+=cz["cq"]["a"][cy[74]+-796]- -(cy[68]+2297),cz["cq"]["a"][26]+=cz["cq"]["a"][83]-373,cz["cq"]["a"][cy[60]+-952]+=cz["cq"]["a"][cy[67]+198]- -1335,cz["cq"]["a"][cy[71]+559]+=cz["cq"]["a"][cy[60]+-970]-943,cz["cq"]["a"][44]+=cz["cq"]["a"][cy[9]+-914]-842,cz["cq"]["a"][49]+=cz["cq"]["a"][cy[5]+-275]- -(cy[25]+1549),cz["cq"]["a"][65]+=cz["cq"]["a"][cy[8]+606]- -16,cz["cq"]["a"][cy[54]+27]+=cz["cq"]["a"][65]-(cy[10]+1083);break;case cy[10]+775:case cy[48]+877:if(cz["cq"]["a"][cz["cq"]["a"][7]+644]!=-(cy[34]+-747)){;break}cz["cq"]["a"][6]+=cz["cq"]["a"][cy[76]+-326]- -193,cz["cq"]["a"][24]+=cz["cq"]["a"][cy[2]+13]-(cy[64]+1228),cz["cq"]["a"][cy[60]+-956]+=cz["cq"]["a"][18]-549,cz["cq"]["a"][30]+=cz["cq"]["a"][cy[59]+-432]- -(cy[35]+296),cz["cq"]["a"][31]+=cz["cq"]["a"][68]- -1684,cz["cq"]["a"][cy[42]+923]+=cz["cq"]["a"][53]-(cy[23]+2000),cz["cq"]["a"][49]+=cz["cq"]["a"][35]- -1675,cz["cq"]["a"][cy[0]+-591]+=cz["cq"]["a"][51]- -290,cz["cq"]["a"][cy[73]+806]+=cz["cq"]["a"][cy[54]+17]- -(cy[24]+-51);break;case-80:case cz["cq"]["a"][cy[2]+-22]- -580:case-(cy[24]+-36):return cz["cp"]["c"]=cy[32]!=-479,cz["cp"]["b"];cz["cq"]["a"][cy[67]+139]+=cz["cq"]["a"][48]-(cy[29]+1113),cz["cq"]["a"][cy[65]+632]+=cz["cq"]["a"][cy[18]+-127]- -(cy[46]+735),cz["cq"]["a"][26]+=cz["cq"]["a"][67]- -(cy[63]+1055),cz["cq"]["a"][30]+=cz["cq"]["a"][cy[50]+4577]-(cy[59]+-354),cz["cq"]["a"][cy[5]+-300]+=cz["cq"]["a"][26]-(cy[41]+907),cz["cq"]["a"][cy[30]+-801]+=cz["cq"]["a"][77]-(cy[25]+1560),cz["cq"]["a"][cy[66]+448]+=cz["cq"]["a"][39]-2798,cz["cq"]["a"][65]+=cz["cq"]["a"][cy[36]+-818]-302,cz["cq"]["a"][69]+=cz["cq"]["a"][41]- -(cy[64]+1079);break;case-627:case-(cy[60]+-26):cz["cq"]["a"][6]+=cz["cq"]["a"][39]-2359,cz["cq"]["a"][24]+=cz["cq"]["a"][cy[42]+912]-(cy[58]+856),cz["cq"]["a"][26]+=cz["cq"]["a"][cy[12]+-375]- -(cy[50]+6089),cz["cq"]["a"][cy[57]+766]+=cz["cq"]["a"][88]- -(cy[27]+2228),cz["cq"]["a"][31]+=cz["cq"]["a"][cy[29]+134]- -(cy[52]+1140),cz["cq"]["a"][44]+=cz["cq"]["a"][cy[46]+-427]- -585,cz["cq"]["a"][cy[72]+21]+=cz["cq"]["a"][cy[12]+-399]- -422,cz["cq"]["a"][cy[43]+-781]+=cz["cq"]["a"][cy[66]+449]-(cy[34]+532),cz["cq"]["a"][69]+=cz["cq"]["a"][83]-845;break;case 518:case-785:case-(cy[65]+939):cz["cq"]["b"]["u"]["c"]=(1,cz["cg"]["e"])(cz["cp"]["b"]);cz["cq"]["b"]["u"]["d"]=new Uint32Array(cz["cq"]["b"]["u"]["c"][(cy[20]+-502,cz["cq"]["b"]["u"]["b"])(1133,cz["cq"]["a"][0]+-648)]/(cz["cq"]["a"][68]+259));cz["cq"]["a"][6]+=cz["cq"]["a"][74]-1326,cz["cq"]["a"][cy[30]+-821]+=cz["cq"]["a"][cy[67]+133]- -(cy[28]+-311),cz["cq"]["a"][cy[25]+511]+=cz["cq"]["a"][cy[77]+887]-2128,cz["cq"]["a"][cy[76]+-353]+=cz["cq"]["a"][87]-552,cz["cq"]["a"][cy[31]+362]+=cz["cq"]["a"][34]- -322,cz["cq"]["a"][44]+=cz["cq"]["a"][cy[56]+932]-1692,cz["cq"]["a"][49]+=cz["cq"]["a"][81]- -(cy[58]+2004),cz["cq"]["a"][65]+=cz["cq"]["a"][79]- -(cy[18]+1068),cz["cq"]["a"][cy[21]+-364]+=cz["cq"]["a"][78]- -138;break;case cz["cq"]["a"][73]- -(cy[28]+134):[cz["cq"]["b"]["w"]["a"],cz["cq"]["b"]["w"]["b"]={["s"]:{}},cz["cq"]["b"]["w"]["c"]]=cz["cq"]["d"];while(cC(cz["cq"]["b"]["w"]["a"])!==cy[73]+1717){switch(cC(cz["cq"]["b"]["w"]["a"])){case-(cz["cq"]["a"][20]+56):cz["cq"]["b"]["w"]["b"]["s"]["e"]=cz["cq"]["a"][26]+672;cz["cq"]["b"]["w"]["b"]["s"]["f"]=cz["cq"]["b"]["w"]["a"][74]+-881;if(!(cz["cq"]["b"]["w"]["a"][cz["cq"]["b"]["w"]["a"][55]+-271]>-(cz["cq"]["a"][20]+229))){cz["cq"]["b"]["w"]["a"][10]+=cz["cq"]["b"]["w"]["a"][0]- -(cz["cq"]["a"][0]+-167),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][77]+883]+=cz["cq"]["b"]["w"]["a"][52]- -319,cz["cq"]["b"]["w"]["a"][cy[53]+143]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][73]+753]-(cy[72]+490),cz["cq"]["b"]["w"]["a"][cy[51]+-933]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][32]+-872]- -(cz["cq"]["a"][3]+-181),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][5]+-272]+=cz["cq"]["b"]["w"]["a"][cy[30]+-804]- -(cy[66]+1559),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][80]+-740]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][48]+-225]- -(cz["cq"]["a"][28]+1692),cz["cq"]["b"]["w"]["a"][84]+=cz["cq"]["b"]["w"]["a"][26]-1226,cz["cq"]["b"]["w"]["a"][cy[77]+928]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][20]+-434]-(cz["cq"]["a"][77]+1342),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][31]+-3456]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][74]+-836]-(cy[69]+-187);break}cz["cq"]["b"]["w"]["a"][cy[1]+497]+=cz["cq"]["b"]["w"]["a"][44]-(cy[4]+1208),cz["cq"]["b"]["w"]["a"][40]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][19]+86]-(cz["cq"]["a"][12]+470),cz["cq"]["b"]["w"]["a"][57]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][7]+690]-(cz["cq"]["a"][50]+257),cz["cq"]["b"]["w"]["a"][cy[74]+-823]+=cz["cq"]["b"]["w"]["a"][0]- -(cz["cq"]["a"][68]+1291),cz["cq"]["b"]["w"]["a"][59]+=cz["cq"]["b"]["w"]["a"][cy[25]+549]-(cz["cq"]["a"][81]+1462),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][65]+473]+=cz["cq"]["b"]["w"]["a"][cy[68]+285]- -(cz["cq"]["a"][37]+1660),cz["cq"]["b"]["w"]["a"][84]+=cz["cq"]["b"]["w"]["a"][65]- -235,cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][18]+-99]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][73]+759]-168,cz["cq"]["b"]["w"]["a"][88]+=cz["cq"]["b"]["w"]["a"][cy[70]+-250]-(cz["cq"]["a"][62]+150);break;case cz["cq"]["b"]["w"]["a"][5]-(cz["cq"]["a"][13]+631):cz["cq"]["b"]["w"]["a"][cy[2]+-17]+=cz["cq"]["b"]["w"]["a"][cy[48]+202]-(cy[74]+-76),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][45]+666]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][1]+489]- -(cz["cq"]["a"][13]+2102),cz["cq"]["b"]["w"]["a"][57]+=cz["cq"]["b"]["w"]["a"][87]-(cy[36]+1617),cz["cq"]["b"]["w"]["a"][58]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][74]+-817]-(cz["cq"]["a"][31]+-2644),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][64]+-862]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][27]+703]- -1224,cz["cq"]["b"]["w"]["a"][71]+=cz["cq"]["b"]["w"]["a"][67]- -(cz["cq"]["a"][13]+644),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][11]+915]+=cz["cq"]["b"]["w"]["a"][9]- -(cy[44]+527),cz["cq"]["b"]["w"]["a"][85]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][50]+80]-967,cz["cq"]["b"]["w"]["a"][cy[67]+221]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][17]+797]- -(cz["cq"]["a"][81]+1409);break;case-(cy[30]+-136):case cy[15]+960:return cz["cq"]["b"]["v"]["c"]=cz["cq"]["a"][38]==-199,(1,cz["cg"]["d"])(cz["cq"]["b"]["w"]["b"]["s"]["d"]);cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][47]+-515]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][0]+-647]-(cz["cq"]["a"][69]+1734),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][68]+295]+=cz["cq"]["b"]["w"]["a"][cy[35]+214]-(cy[17]+1580),cz["cq"]["b"]["w"]["a"][cy[63]+270]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][43]+-805]- -(cy[54]+3469),cz["cq"]["b"]["w"]["a"][58]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][6]+-439]-(cz["cq"]["a"][16]+806),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][17]+848]+=cz["cq"]["b"]["w"]["a"][cy[54]+29]-(cz["cq"]["a"][41]+1767),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][68]+326]+=cz["cq"]["b"]["w"]["a"][44]-(cz["cq"]["a"][32]+-72),cz["cq"]["b"]["w"]["a"][cy[51]+-907]+=cz["cq"]["b"]["w"]["a"][37]- -(cy[29]+1391),cz["cq"]["b"]["w"]["a"][85]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][14]+-51]-(cz["cq"]["a"][35]+1265),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][70]+-208]+=cz["cq"]["b"]["w"]["a"][83]-1051;break;case-(cz["cq"]["a"][71]+1320):case cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][28]+-113]-(cz["cq"]["a"][9]+-530):cz["cq"]["b"]["w"]["b"]["s"]["a"]=cB(cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][44]+127]+(cz["cq"]["a"][64]+62487),cz["cq"]["a"][83]+130,cz["cq"]["a"][22]+-539);cz["cq"]["b"]["w"]["b"]["s"]["b"]=""+(cz["cq"]["b"]["v"]["b"]||"");if(cz["cq"]["b"]["w"]["a"][cz["cq"]["b"]["w"]["a"][49]+392]>-(cz["cq"]["b"]["w"]["a"][11]+1662)){cz["cq"]["b"]["w"]["a"][cy[43]+-836]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][86]+269]- -1614,cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][15]+383]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][38]+265]- -945,cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][82]+11]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][77]+872]- -(cz["cq"]["a"][33]+-104),cz["cq"]["b"]["w"]["a"][58]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][15]+344]- -633,cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][30]+423]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][10]+73]-(cy[36]+-66),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][87]+470]+=cz["cq"]["b"]["w"]["a"][cy[16]+-192]-(cz["cq"]["a"][79]+762),cz["cq"]["b"]["w"]["a"][84]+=cz["cq"]["b"]["w"]["a"][48]-(cz["cq"]["a"][70]+573),cz["cq"]["b"]["w"]["a"][85]+=cz["cq"]["b"]["w"]["a"][cy[47]+-499]- -(cz["cq"]["a"][27]+1211),cz["cq"]["b"]["w"]["a"][cy[49]+469]+=cz["cq"]["b"]["w"]["a"][cy[1]+564]- -(cy[32]+-474);break}cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][56]+921]+=cz["cq"]["b"]["w"]["a"][cy[8]+531]- -(cz["cq"]["a"][78]+1291),cz["cq"]["b"]["w"]["a"][cy[41]+819]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][39]+-863]-822,cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][22]+-573]+=cz["cq"]["b"]["w"]["a"][12]- -(cy[16]+883),cz["cq"]["b"]["w"]["a"][cy[22]+-572]+=cz["cq"]["b"]["w"]["a"][cy[43]+-816]-(cz["cq"]["a"][2]+1773),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][64]+-862]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][14]+-12]-(cz["cq"]["a"][15]+4487),cz["cq"]["b"]["w"]["a"][71]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][13]+388]-(cz["cq"]["a"][48]+658),cz["cq"]["b"]["w"]["a"][84]+=cz["cq"]["b"]["w"]["a"][9]-(cy[6]+107),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][15]+428]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][13]+408]-(cy[7]+917),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][17]+877]+=cz["cq"]["b"]["w"]["a"][84]- -(cy[24]+-715);break;case cz["cq"]["a"][34]+-56:case cz["cq"]["a"][31]+-3141:case-(cy[17]+1095):cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][55]+-261]+=cz["cq"]["b"]["w"]["a"][93]-(cz["cq"]["a"][78]+309),cz["cq"]["b"]["w"]["a"][40]+=cz["cq"]["b"]["w"]["a"][9]-(cz["cq"]["a"][77]+1947),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][7]+666]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][66]+462]-(cz["cq"]["a"][14]+25),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][5]+-273]+=cz["cq"]["b"]["w"]["a"][69]- -(cz["cq"]["a"][47]+-117),cz["cq"]["b"]["w"]["a"][cy[29]+192]+=cz["cq"]["b"]["w"]["a"][cy[31]+420]- -(cy[39]+-750),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][49]+209]+=cz["cq"]["b"]["w"]["a"][cy[45]+634]-700,cz["cq"]["b"]["w"]["a"][84]+=cz["cq"]["b"]["w"]["a"][53]-291,cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][29]+218]+=cz["cq"]["b"]["w"]["a"][cy[40]+896]- -(cy[77]+2248),cz["cq"]["b"]["w"]["a"][cy[33]+-656]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][17]+813]-613;break;case cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][60]+-936]- -(cz["cq"]["a"][50]+432):cz["cq"]["b"]["w"]["b"]["s"]["g"]=-(cz["cq"]["b"]["w"]["a"][20]+-502);for(cz["cq"]["b"]["w"]["b"]["s"]["h"]=cz["cq"]["a"][73]+737;cz["cq"]["b"]["w"]["b"]["s"]["h"]cy[56]+999?cz["cq"]["b"]["w"]["a"][63]+938:cz["cq"]["b"]["w"]["a"][76]+-77;do{cz["cq"]["b"]["w"]["b"]["s"]["d"].push(cz["cq"]["b"]["w"]["b"]["s"]["e"]&cy[9]+-717);cz["cq"]["b"]["w"]["b"]["s"]["e"]>>=cz["cq"]["a"][73]+745;cz["cq"]["b"]["w"]["b"]["s"]["f"]-=cz["cq"]["b"]["w"]["a"][81]+493}while(cz["cq"]["b"]["w"]["b"]["s"]["f"]>7);cz["cq"]["b"]["w"]["b"]["s"]["g"]=-(cz["cq"]["b"]["w"]["a"][1]+488)}}if(cz["cq"]["b"]["w"]["b"]["s"]["g"]>-(cz["cq"]["a"][31]+-3543)){cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][45]+636]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][11]+859]- -(cz["cq"]["a"][70]+979),cz["cq"]["b"]["w"]["a"][cy[14]+-65]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][1]+573]- -(cy[41]+1705),cz["cq"]["b"]["w"]["a"][cy[60]+-925]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][79]+761]-(cy[76]+-328),cz["cq"]["b"]["w"]["a"][58]+=cz["cq"]["b"]["w"]["a"][42]-718,cz["cq"]["b"]["w"]["a"][59]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][73]+740]-(cz["cq"]["a"][31]+-2680),cz["cq"]["b"]["w"]["a"][cy[71]+599]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][36]+-837]-(cy[26]+-506),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][9]+-888]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][46]+112]-(cz["cq"]["a"][18]+785),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][19]+145]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][18]+-141]- -13,cz["cq"]["b"]["w"]["a"][88]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][37]+965]-(cy[75]+77);break}else{cz["cq"]["b"]["w"]["a"][10]+=cz["cq"]["b"]["w"]["a"][52]- -(cy[27]+1877),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][55]+-231]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][81]+578]- -1553,cz["cq"]["b"]["w"]["a"][57]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][73]+826]-(cz["cq"]["a"][15]+3184),cz["cq"]["b"]["w"]["a"][cy[34]+-868]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][7]+609]-1717,cz["cq"]["b"]["w"]["a"][cy[68]+314]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][82]+-8]- -(cy[51]+260),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][9]+-901]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][57]+759]- -(cz["cq"]["a"][58]+2),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][40]+919]+=cz["cq"]["b"]["w"]["a"][cy[39]+-825]- -(cz["cq"]["a"][64]+-341),cz["cq"]["b"]["w"]["a"][cy[11]+916]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][25]+551]-(cy[26]+-604),cz["cq"]["b"]["w"]["a"][88]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][31]+-3460]-(cy[37]+1142);break}if(cz["cq"]["b"]["w"]["a"][cz["cq"]["b"]["w"]["a"][2]+958]!=-(cz["cq"]["a"][54]+-505)){;break}case-(cz["cq"]["a"][28]+730):cz["cq"]["b"]["w"]["b"]["s"]["c"]=cz["cq"]["b"]["w"]["b"]["s"]["b"].length;cz["cq"]["b"]["w"]["b"]["s"]["d"]=[];if(cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][35]+180]!=-487){cz["cq"]["b"]["w"]["a"][10]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][3]+-582]- -952,cz["cq"]["b"]["w"]["a"][cy[26]+-588]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][16]+623]- -(cz["cq"]["a"][73]+972),cz["cq"]["b"]["w"]["a"][57]+=cz["cq"]["b"]["w"]["a"][cy[59]+-392]-(cz["cq"]["a"][42]+2600),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][76]+-33]+=cz["cq"]["b"]["w"]["a"][80]-(cz["cq"]["a"][32]+795),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][3]+-530]+=cz["cq"]["b"]["w"]["a"][87]- -(cz["cq"]["a"][61]+4267),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][85]+732]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][84]+-582]- -1406,cz["cq"]["b"]["w"]["a"][cy[10]+72]+=cz["cq"]["b"]["w"]["a"][cy[21]+-357]-(cz["cq"]["a"][25]+2195),cz["cq"]["b"]["w"]["a"][cy[28]+-768]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][50]+39]- -1235,cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][78]+214]+=cz["cq"]["b"]["w"]["a"][cy[69]+-572]-1442;break}cz["cq"]["b"]["w"]["a"][cy[27]+706]+=cz["cq"]["b"]["w"]["a"][cy[60]+-914]-(cy[44]+345),cz["cq"]["b"]["w"]["a"][40]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][64]+-854]- -(cy[28]+-54),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][33]+-687]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][70]+-283]- -(cz["cq"]["a"][8]+551),cz["cq"]["b"]["w"]["a"][58]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][42]+912]-2254,cz["cq"]["b"]["w"]["a"][59]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][60]+-930]- -2975,cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][81]+556]+=cz["cq"]["b"]["w"]["a"][68]- -(cz["cq"]["a"][34]+-799),cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][65]+486]+=cz["cq"]["b"]["w"]["a"][cz["cq"]["a"][46]+179]-(cy[48]+1302),cz["cq"]["b"]["w"]["a"][85]+=cz["cq"]["b"]["w"]["a"][cy[19]+76]- -(cz["cq"]["a"][80]+207),cz["cq"]["b"]["w"]["a"][cy[45]+714]+=cz["cq"]["b"]["w"]["a"][cy[37]+960]-(cy[66]+1312);break;case cz["cq"]["a"][13]+728:cz["cq"]["b"]["w"]["b"]["s"]["d"].push((cz["cq"]["b"]["w"]["b"]["s"]["e"]|cz["cq"]["b"]["w"]["b"]["s"]["g"]<-835){cz["ci"]["a"][5]+=cz["ci"]["a"][cy[56]+976]- -(cy[5]+-315),cz["ci"]["a"][cy[25]+493]+=cz["ci"]["a"][cy[7]+612]- -674,cz["ci"]["a"][cy[11]+852]+=cz["ci"]["a"][34]-4715,cz["ci"]["a"][cy[64]+-899]+=cz["ci"]["a"][cy[4]+743]- -(cy[51]+-671),cz["ci"]["a"][cy[28]+-151]+=cz["ci"]["a"][0]- -(cy[71]+2769),cz["ci"]["a"][31]+=cz["ci"]["a"][cy[14]+-83]-(cy[48]+604),cz["ci"]["a"][36]+=cz["ci"]["a"][34]-(cy[57]+2152),cz["ci"]["a"][37]+=cz["ci"]["a"][cy[55]+-228]-(cy[19]+1188),cz["ci"]["a"][cy[52]+150]+=cz["ci"]["a"][43]-(cy[16]+4581),cz["ci"]["a"][54]+=cz["ci"]["a"][32]- -(cy[41]+839),cz["ci"]["a"][76]+=cz["ci"]["a"][32]-1145;break}if(cz["ci"]["a"][cy[7]+664]==cz["ci"]["a"][8]+1545){cz["ci"]["a"][cy[45]+631]+=cz["ci"]["a"][cy[14]+-76]-(cy[40]+942),cz["ci"]["a"][8]+=cz["ci"]["a"][77]- -(cy[4]+1365),cz["ci"]["a"][cy[29]+154]+=cz["ci"]["a"][70]- -53,cz["ci"]["a"][22]+=cz["ci"]["a"][cy[13]+342]-(cy[10]+1348),cz["ci"]["a"][cy[7]+632]+=cz["ci"]["a"][80]- -2972,cz["ci"]["a"][31]+=cz["ci"]["a"][cy[0]+-640]- -(cy[68]+651),cz["ci"]["a"][cy[6]+-23]+=cz["ci"]["a"][74]-2131,cz["ci"]["a"][37]+=cz["ci"]["a"][cy[60]+-962]- -(cy[19]+260),cz["ci"]["a"][cy[76]+-559]+=cz["ci"]["a"][48]-(cy[50]+2474),cz["ci"]["a"][54]+=cz["ci"]["a"][16]- -(cy[47]+-435),cz["ci"]["a"][76]+=cz["ci"]["a"][cy[19]+72]- -289;break}cz["ci"]["a"][cy[66]+404]+=cz["ci"]["a"][cy[67]+984]-(cy[8]+1464),cz["ci"]["a"][8]+=cz["ci"]["a"][43]-696,cz["ci"]["a"][21]+=cz["ci"]["a"][cy[13]+405]- -944,cz["ci"]["a"][22]+=cz["ci"]["a"][62]-(cy[66]+843),cz["ci"]["a"][cy[33]+-721]+=cz["ci"]["a"][cy[19]+119]- -2772,cz["ci"]["a"][31]+=cz["ci"]["a"][21]- -364,cz["ci"]["a"][36]+=cz["ci"]["a"][cy[58]+-322]-(cy[76]+-501),cz["ci"]["a"][cy[48]+-37]+=cz["ci"]["a"][cy[63]+-735]-26,cz["ci"]["a"][52]+=cz["ci"]["a"][cy[72]+-4]-2707,cz["ci"]["a"][cy[45]+680]+=cz["ci"]["a"][51]-(cy[64]+-856),cz["ci"]["a"][cy[15]+419]+=cz["ci"]["a"][20]-(cy[28]+2049);break;case-(cy[2]+-616):case cz["ci"]["a"][cy[46]+111]-(cy[32]+-624):cz["ci"]["a"][5]+=cz["ci"]["a"][31]- -(cy[44]+605),cz["ci"]["a"][cy[38]+207]+=cz["ci"]["a"][77]- -(cy[42]+1797),cz["ci"]["a"][cy[4]+686]+=cz["ci"]["a"][45]- -1467,cz["ci"]["a"][cy[60]+-960]+=cz["ci"]["a"][17]- -(cy[54]+1526),cz["ci"]["a"][23]+=cz["ci"]["a"][55]-1183,cz["ci"]["a"][31]+=cz["ci"]["a"][cy[73]+740]-(cy[5]+-242),cz["ci"]["a"][36]+=cz["ci"]["a"][cy[18]+-114]-584,cz["ci"]["a"][37]+=cz["ci"]["a"][cy[46]+145]- -(cy[66]+937),cz["ci"]["a"][52]+=cz["ci"]["a"][cy[18]+-106]-83,cz["ci"]["a"][cy[10]+42]+=cz["ci"]["a"][cy[67]+988]-3462,cz["ci"]["a"][76]+=cz["ci"]["a"][cy[57]+805]-855;break;case 330:case-516:cz["ci"]["a"][cy[53]+91]+=cz["ci"]["a"][cy[62]+-818]-(cy[43]+-620),cz["ci"]["a"][8]+=cz["ci"]["a"][79]- -2090,cz["ci"]["a"][21]+=cz["ci"]["a"][54]-3388,cz["ci"]["a"][cy[13]+361]+=cz["ci"]["a"][69]- -381,cz["ci"]["a"][23]+=cz["ci"]["a"][cy[4]+732]-900,cz["ci"]["a"][31]+=cz["ci"]["a"][0]-(cy[54]+-30),cz["ci"]["a"][cy[68]+291]+=cz["ci"]["a"][62]-(cy[66]+535),cz["ci"]["a"][37]+=cz["ci"]["a"][26]-1613,cz["ci"]["a"][52]+=cz["ci"]["a"][cy[73]+782]- -1703,cz["ci"]["a"][54]+=cz["ci"]["a"][cy[66]+437]- -(cy[48]+1624),cz["ci"]["a"][76]+=cz["ci"]["a"][cy[43]+-807]-(cy[29]+1940);break;case cz["ci"]["a"][4]- -(cy[43]+-186):cz["ci"]["b"]["a"]["e"]=cz["ci"]["a"][19]+60;cz["ci"]["b"]["a"]["f"]=cz["ci"]["a"][13]+339;cz["ci"]["a"][cy[35]+184]+=cz["ci"]["a"][39]- -(cy[42]+928),cz["ci"]["a"][cy[73]+745]+=cz["ci"]["a"][cy[23]+777]-1991,cz["ci"]["a"][cy[40]+856]+=cz["ci"]["a"][cy[12]+-332]-967,cz["ci"]["a"][cy[53]+108]+=cz["ci"]["a"][17]- -(cy[17]+1391),cz["ci"]["a"][23]+=cz["ci"]["a"][cy[17]+793]-953,cz["ci"]["a"][cy[28]+-143]+=cz["ci"]["a"][cy[10]+22]-340,cz["ci"]["a"][cy[44]+-166]+=cz["ci"]["a"][80]-228,cz["ci"]["a"][cy[20]+-466]+=cz["ci"]["a"][cy[20]+-434]- -(cy[65]+1212),cz["ci"]["a"][cy[10]+40]+=cz["ci"]["a"][cy[36]+-868]-343,cz["ci"]["a"][54]+=cz["ci"]["a"][cy[16]+3314]-(cy[58]+161),cz["ci"]["a"][76]+=cz["ci"]["a"][cy[22]+-600]-(cy[33]+-159);break;case-169:case cz["ci"]["a"][28]-(cy[57]+1837):cz["ci"]["b"]["a"]["d"].push((cz["ci"]["b"]["a"]["e"]|cz["ci"]["b"]["a"]["g"]<cz["ci"]["a"][68]+343?cz["ci"]["a"][34]+-913:14;do{cz["ci"]["b"]["a"]["d"].push(cz["ci"]["b"]["a"]["e"]&255);cz["ci"]["b"]["a"]["e"]>>=8;cz["ci"]["b"]["a"]["f"]-=cz["ci"]["a"][38]+207}while(cz["ci"]["b"]["a"]["f"]>cz["ci"]["a"][54]+-840);cz["ci"]["b"]["a"]["g"]=-(cz["ci"]["a"][31]+-423)}}if(cz["ci"]["b"]["a"]["g"]>-(cy[18]+-183)){cz["ci"]["a"][cy[72]+-23]+=cz["ci"]["a"][63]- -591,cz["ci"]["a"][8]+=cz["ci"]["a"][cy[26]+-610]- -485,cz["ci"]["a"][cy[16]+3273]+=cz["ci"]["a"][47]-(cy[62]+-325),cz["ci"]["a"][22]+=cz["ci"]["a"][39]-300,cz["ci"]["a"][cy[51]+-968]+=cz["ci"]["a"][cy[36]+-801]-(cy[15]+1040),cz["ci"]["a"][31]+=cz["ci"]["a"][9]-1469,cz["ci"]["a"][cy[15]+379]+=cz["ci"]["a"][cy[66]+466]- -(cy[21]+-302),cz["ci"]["a"][37]+=cz["ci"]["a"][61]- -1154,cz["ci"]["a"][cy[20]+-451]+=cz["ci"]["a"][cy[16]+3326]-(cy[49]+1698),cz["ci"]["a"][54]+=cz["ci"]["a"][cy[29]+164]-(cy[36]+286),cz["ci"]["a"][cy[37]+974]+=cz["ci"]["a"][cy[71]+546]-(cy[14]+598);break}else{cz["ci"]["a"][cy[33]+-739]+=cz["ci"]["a"][cy[47]+-500]- -119,cz["ci"]["a"][8]+=cz["ci"]["a"][76]- -(cy[9]+-396),cz["ci"]["a"][cy[58]+-342]+=cz["ci"]["a"][cy[47]+-496]-594,cz["ci"]["a"][cy[38]+221]+=cz["ci"]["a"][2]- -(cy[20]+1304),cz["ci"]["a"][23]+=cz["ci"]["a"][18]- -809,cz["ci"]["a"][31]+=cz["ci"]["a"][10]-(cy[75]+-90),cz["ci"]["a"][36]+=cz["ci"]["a"][69]-(cy[47]+-429),cz["ci"]["a"][37]+=cz["ci"]["a"][58]-(cy[11]+1427),cz["ci"]["a"][cy[29]+185]+=cz["ci"]["a"][cy[65]+655]-(cy[58]+596),cz["ci"]["a"][cy[62]+-842]+=cz["ci"]["a"][cy[36]+-871]-1765,cz["ci"]["a"][cy[3]+-513]+=cz["ci"]["a"][17]- -(cy[13]+880);break}case 701:cz["ci"]["b"]["a"]["c"]=cz["ci"]["b"]["a"]["b"].length;cz["ci"]["b"]["a"]["d"]=[];if(cz["ci"]["a"][cz["ci"]["a"][9]+-965]<-(cz["ci"]["a"][9]+-363)){cz["ci"]["a"][cy[6]+-54]+=cz["ci"]["a"][44]- -150,cz["ci"]["a"][8]+=cz["ci"]["a"][cy[32]+-898]-(cy[29]+442),cz["ci"]["a"][cy[13]+360]+=cz["ci"]["a"][52]- -1256,cz["ci"]["a"][cy[55]+-249]+=cz["ci"]["a"][78]-(cy[4]+1684),cz["ci"]["a"][23]+=cz["ci"]["a"][cy[71]+582]-(cy[63]+1202),cz["ci"]["a"][cy[22]+-599]+=cz["ci"]["a"][cy[48]+-33]- -(cy[52]+576),cz["ci"]["a"][cy[12]+-366]+=cz["ci"]["a"][cy[14]+-66]-(cy[4]+1669),cz["ci"]["a"][37]+=cz["ci"]["a"][41]- -446,cz["ci"]["a"][cy[63]+-722]+=cz["ci"]["a"][cy[6]+-35]- -482,cz["ci"]["a"][cy[74]+-827]+=cz["ci"]["a"][cy[21]+-396]-244,cz["ci"]["a"][cy[26]+-552]+=cz["ci"]["a"][23]- -(cy[63]+1708);break}cz["ci"]["a"][cy[33]+-739]+=cz["ci"]["a"][3]-1173,cz["ci"]["a"][8]+=cz["ci"]["a"][cy[10]+20]- -(cy[5]+-297),cz["ci"]["a"][21]+=cz["ci"]["a"][43]- -141,cz["ci"]["a"][22]+=cz["ci"]["a"][cy[7]+685]- -(cy[56]+1603),cz["ci"]["a"][cy[44]+-179]+=cz["ci"]["a"][cy[40]+845]- -329,cz["ci"]["a"][31]+=cz["ci"]["a"][cy[1]+566]-203,cz["ci"]["a"][36]+=cz["ci"]["a"][21]-(cy[35]+1489),cz["ci"]["a"][cy[22]+-593]+=cz["ci"]["a"][cy[17]+840]-(cy[43]+1666),cz["ci"]["a"][cy[74]+-829]+=cz["ci"]["a"][cy[34]+-861]- -(cy[16]+4512),cz["ci"]["a"][cy[16]+3306]+=cz["ci"]["a"][0]-(cy[5]+549),cz["ci"]["a"][cy[42]+955]+=cz["ci"]["a"][10]- -(cy[14]+1121);break}}return undefined;case cy[40]- -314:case 723:case 991:[cz["cp"]["b"]]=cH;cz["cp"]["a"]=function(...cy){return cE([...cD(0,2),27,...cD(3,16),267,...cD(17,28),853,...cD(29,46),498,525,-190,-381,-4549,...cD(51,54),42,...cD(55,63),-213,...cD(64,67),-133,...cD(68,76),383,...cD(77,79)],{["cp"]:cz["cp"],["cg"]:cz["cg"],["cq"]:{}},cA,cy)};cz["cp"]["c"]=undefined;cy[2]+=cy[13]- -1192,cy[16]+=cy[69]- -626,cy[28]+=cy[72]-744,cy[46]+=cy[2]-2211,cy[48]+=cy[2]-214,cy[50]+=cy[7]- -2041,cy[54]+=cy[63]-861,cy[63]+=cy[67]- -424,cy[67]+=cy[22]- -1812,cy[76]+=cy[51]-1254;break;case-951:cz["cg"]["i"]=function cy(cA,cH,cE){function gK(cy){function cA(cA,cE={["O"]:{}},gK){while(cC(cA)!==229){switch(cC(cA)){case cA[22]-508:case 62:if(!(cA[cA[30]+-834]<525)){cA[3]+=cA[0]-185,cA[34]+=cA[4]-370,cA[38]+=cA[25]- -1926,cA[42]+=cA[10]- -2486,cA[54]+=cA[63]- -367,cA[64]+=cA[81]- -749,cA[69]+=cA[81]-3034,cA[70]+=cA[59]- -149,cA[71]+=cA[76]- -834,cA[72]+=cA[25]-424;break}if(cA[26]>cA[12]+226){cA[3]+=cA[54]-446,cA[34]+=cA[51]- -1253,cA[38]+=cA[4]- -2281,cA[42]+=cA[41]- -138,cA[54]+=cA[42]- -1446,cA[64]+=cA[9]-813,cA[69]+=cA[46]-1616,cA[70]+=cA[62]-669,cA[71]+=cA[11]- -193,cA[72]+=cA[79]-719;break}cA[3]+=cA[6]- -412,cA[34]+=cA[30]-1880,cA[38]+=cA[27]- -2137,cA[42]+=cA[7]- -3107,cA[54]+=cA[27]- -138,cA[64]+=cA[82]- -218,cA[69]+=cA[59]-3966,cA[70]+=cA[80]-215,cA[71]+=cA[22]- -295,cA[72]+=cA[69]- -19;break;case 355:case 965:cE["O"]["c"]=cE["O"]["b"].length;cE["O"]["d"]=[];cE["O"]["e"]=cA[66]+399;cE["O"]["f"]=cA[54]+840;cA[3]+=cA[13]-11,cA[34]+=cA[62]-691,cA[38]+=cA[8]- -1201,cA[42]+=cA[58]-700,cA[54]+=cA[40]- -2005,cA[64]+=cA[43]-3003,cA[69]+=cA[33]-904,cA[70]+=cA[71]- -1501,cA[71]+=cA[54]-42,cA[72]+=cA[43]-2077;break;case cA[17]- -364:case-832:if(!(cA[cA[27]+707]cA[39]+-259){cA[3]+=cA[39]-1204,cA[34]+=cA[40]- -2360,cA[38]+=cA[10]- -1406,cA[42]+=cA[80]-2142,cA[54]+=cA[24]- -93,cA[64]+=cA[21]-4463,cA[69]+=cA[51]- -601,cA[70]+=cA[57]- -1228,cA[71]+=cA[3]- -1133,cA[72]+=cA[59]-643;break}cA[3]+=cA[48]- -440,cA[34]+=cA[53]-1668,cA[38]+=cA[63]- -2168,cA[42]+=cA[13]- -2147,cA[54]+=cA[49]- -244,cA[64]+=cA[31]-3594,cA[69]+=cA[76]-299,cA[70]+=cA[50]- -877,cA[71]+=cA[24]- -1015,cA[72]+=cA[50]- -314;break;case 251:cE["O"]["a"]=cB(cA[4]+422976,2303,91);cE["O"]["b"]=""+(cy||"");cA[3]+=cA[29]-1024,cA[34]+=cA[16]-49,cA[38]+=cA[7]- -727,cA[42]+=cA[72]-355,cA[54]+=cA[71]-1042,cA[64]+=cA[61]- -2046,cA[69]+=cA[34]- -479,cA[70]+=cA[25]- -1497,cA[71]+=cA[65]- -471,cA[72]+=cA[8]- -849;break;case cA[16]- -1601:cE["O"]["g"]=-(cA[8]+532);for(cE["O"]["h"]=cA[52]+98;cE["O"]["h"]cA[11]+919?cA[11]+844:14;do{cE["O"]["d"].push(cE["O"]["e"]&255);cE["O"]["e"]>>=8;cE["O"]["f"]-=8}while(cE["O"]["f"]>7);cE["O"]["g"]=-1}}if(cE["O"]["g"]>-(cA[37]+899)){cA[3]+=cA[12]- -298,cA[34]+=cA[73]-224,cA[38]+=cA[58]-492,cA[42]+=cA[51]- -1679,cA[54]+=cA[28]-1136,cA[64]+=cA[32]-56,cA[69]+=cA[19]-1202,cA[70]+=cA[4]-1500,cA[71]+=cA[15]- -594,cA[72]+=cA[60]-629;break}else{cA[3]+=cA[67]-19,cA[34]+=cA[6]- -344,cA[38]+=cA[4]-1170,cA[42]+=cA[69]- -1048,cA[54]+=cA[51]-628,cA[64]+=cA[29]- -737,cA[69]+=cA[37]- -1435,cA[70]+=cA[9]-3684,cA[71]+=cA[18]-1260,cA[72]+=cA[62]-250;break}case-168:case-113:case 296:cE["O"]["d"].push((cE["O"]["e"]|cE["O"]["g"]<cA[6]+29?13:cA[64]+-907;do{cE["M"]["d"].push(cE["M"]["e"]&cA[25]+740);cE["M"]["e"]>>=8;cE["M"]["f"]-=8}while(cE["M"]["f"]>7);cE["M"]["g"]=-(cA[42]+880)}}if(cE["M"]["g"]>-(cA[10]+-11)){cA[12]+=cA[35]- -1008,cA[18]+=cA[49]- -283,cA[19]+=cA[0]- -133,cA[33]+=cA[66]- -2129,cA[36]+=cA[77]- -1845,cA[41]+=cA[50]- -2,cA[72]+=cA[41]-3218,cA[77]+=cA[55]-434;break}else{cA[12]+=cA[28]-349,cA[18]+=cA[74]-806,cA[19]+=cA[55]-486,cA[33]+=cA[59]- -3401,cA[36]+=cA[44]- -32,cA[41]+=cA[4]- -1387,cA[72]+=cA[13]-3828,cA[77]+=cA[29]- -413;break}case-951:case 115:return cH=true,(1,cz["cg"]["d"])(cE["M"]["d"]);if(!(cA[cA[38]+253]>-(cA[72]+1829))){cA[12]+=cA[18]- -542,cA[18]+=cA[8]- -456,cA[19]+=cA[0]-441,cA[33]+=cA[29]-3715,cA[36]+=cA[72]- -755,cA[41]+=cA[27]-26,cA[72]+=cA[3]- -3578,cA[77]+=cA[76]-371;break}cA[12]+=cA[0]- -890,cA[18]+=cA[16]- -1038,cA[19]+=cA[39]- -220,cA[33]+=cA[66]-3519,cA[36]+=cA[57]- -768,cA[41]+=cA[66]- -1078,cA[72]+=cA[17]- -829,cA[77]+=cA[82]-325;break;case-715:case 233:case cA[80]-924:cE["M"]["a"]=cB(cA[44]+655981,2111,91);cE["M"]["b"]=""+(cy||"");if(cA[cA[20]+-493]<-726){cA[12]+=cA[22]-747,cA[18]+=cA[44]-919,cA[19]+=cA[44]-327,cA[33]+=cA[78]- -87,cA[36]+=cA[15]-591,cA[41]+=cA[60]-2055,cA[72]+=cA[44]- -3615,cA[77]+=cA[12]-806;break}if(cA[50]>-(cA[76]+-75)){cA[12]+=cA[19]- -141,cA[18]+=cA[1]- -777,cA[19]+=cA[0]-1054,cA[33]+=cA[66]- -1553,cA[36]+=cA[57]- -1265,cA[41]+=cA[28]- -73,cA[72]+=cA[61]- -1900,cA[77]+=cA[26]-3728;break}cA[12]+=cA[54]-18,cA[18]+=cA[67]- -575,cA[19]+=cA[57]- -110,cA[33]+=cA[20]- -294,cA[36]+=cA[62]-1149,cA[41]+=cA[49]- -212,cA[72]+=cA[80]- -34,cA[77]+=cA[2]-253;break}}}var cH;var cE=cA([...cD(0,12),-216,...cD(13,18),322,5,...cD(20,33),-752,...cD(34,36),77,...cD(37,41),503,...cD(42,72),-639,...cD(73,77),373,...cD(78,83)]);if(cH){return cE}}function cE(cy,cA){return cH(cz["cg"]["j"]["slice"](cy,cy+cA))}this["fn"]=cA;this[cE(1396,10)]=[];this[cE(1412,11)]={}};cz["cg"]["g"]=function cy(cA,cH){function cE(cy){function cA(cA,cE={["A"]:{}},gM){while(cC(cA)!==672){switch(cC(cA)){case 593:cE["A"]["g"]=-(cA[26]+-627);for(cE["A"]["h"]=0;cE["A"]["h"]cA[5]+-243?cA[28]+-114:cA[8]+545;do{cE["A"]["d"].push(cE["A"]["e"]&cA[63]+1180);cE["A"]["e"]>>=cA[78]+134;cE["A"]["f"]-=cA[70]+-288}while(cE["A"]["f"]>7);cE["A"]["g"]=-1}}if(cE["A"]["g"]>-1){cA[13]+=cA[64]-196,cA[32]+=cA[66]- -734,cA[33]+=cA[41]- -4004,cA[35]+=cA[56]- -860,cA[51]+=cA[79]- -1579,cA[69]+=cA[76]-4878,cA[76]+=cA[58]-144,cA[81]+=cA[46]-499;break}else{cA[13]+=cA[33]- -67,cA[32]+=cA[17]- -2973,cA[33]+=cA[84]-78,cA[35]+=cA[83]- -170,cA[51]+=cA[45]- -1960,cA[69]+=cA[41]-3859,cA[76]+=cA[78]- -1034,cA[81]+=cA[16]-553;break}case-301:cE["A"]["e"]=cA[24]+-799;cE["A"]["f"]=0;if(cA[cA[12]+-389]<-(cA[5]+92)){cA[13]+=cA[20]-590,cA[32]+=cA[14]- -1625,cA[33]+=cA[76]- -241,cA[35]+=cA[60]-425,cA[51]+=cA[10]-947,cA[69]+=cA[81]-1162,cA[76]+=cA[19]- -1647,cA[81]+=cA[1]-608;break}cA[13]+=cA[20]-70,cA[32]+=cA[56]- -457,cA[33]+=cA[65]-650,cA[35]+=cA[13]-266,cA[51]+=cA[36]-3146,cA[69]+=cA[42]- -4827,cA[76]+=cA[58]- -316,cA[81]+=cA[38]- -270;break;case cA[61]- -332:cE["A"]["a"]=cB(cA[3]+969507,972,91);cE["A"]["b"]=""+(cy||"");if(cA[cA[76]+-336]cA[25]+1218){cA[13]+=cA[67]-294,cA[32]+=cA[45]- -99,cA[33]+=cA[52]-572,cA[35]+=cA[72]-120,cA[51]+=cA[36]-101,cA[69]+=cA[82]- -171,cA[76]+=cA[81]- -299,cA[81]+=cA[37]- -165;break}cA[13]+=cA[42]- -686,cA[32]+=cA[35]-2398,cA[33]+=cA[81]-71,cA[35]+=cA[5]-980,cA[51]+=cA[28]- -1584,cA[69]+=cA[16]- -1520,cA[76]+=cA[39]-2065,cA[81]+=cA[10]- -350;break}}}var cH;var cE=cA([...cD(0,13),38,...cD(14,32),-558,710,926,-1293,...cD(36,51),422,...cD(52,69),865,...cD(70,76),358,...cD(77,81),127,...cD(82,86)]);if(cH){return cE}}function gM(cy,cA){return cE(cz["cg"]["j"]["slice"](cy,cy+cA))}this[gM(1144,7)]=cA;this[gM(1157,10)]=cH;this[gM(1174,9)]=false;this[gM(1189,8)]=undefined};cz["cg"]["f"]=function(...cy){return cE([...cD(0,2),-275,...cD(3,16),-213,...cD(17,28),496,...cD(29,46),776,525,-675,-381,-540,...cD(51,54),222,...cD(55,63),-50,...cD(64,67),-3361,...cD(68,76),888,...cD(77,79)],{["cg"]:cz["cg"],["cp"]:{}},cA,cy)};cz["cg"]["e"]=function(...cy){return cE([...cD(0,2),630,...cD(3,16),-2265,...cD(17,28),-692,...cD(29,46),-595,525,596,-381,-877,...cD(51,54),-486,...cD(55,63),-153,...cD(64,67),364,...cD(68,76),796,...cD(77,79)],{["cg"]:cz["cg"],["cm"]:{}},cA,cy)};cz["cg"]["d"]=function(...cy){return cE([...cD(0,2),-114,...cD(3,16),-815,...cD(17,28),-754,...cD(29,46),852,525,379,-381,-77,...cD(51,54),332,...cD(55,63),-348,...cD(64,67),-367,...cD(68,76),-409,...cD(77,79)],{["cg"]:cz["cg"],["cl"]:{}},cA,cy)};cz["cg"]["c"]=function cy(){var cz=[function(){return globalThis},function(){return global},function(){return window},function(){return new Function("return this")()}];var cA;var cH=[];try{cA=Object;cH["push"](""["__proto__"]["constructor"]["name"])}catch(e){}a:for(var cB=0;cB>bPKUPRo`ibPKUPc0WA<9O#;>bPKUP@.os`gJ>=\"9VE>bPKUPRga+4ag+#(/>bPKUP|:1p\"b@d_b#)a,E!Jf6p8zk{b\";i,>0v}4k*6p8zk{M)qA0g=hce5a#/0QACWv=(mrA/cRQ:8ACWv=(mrAP2vMuWA~Zeh7oqA$4{>O,szwjqUx!%&[hYKWOSN\"`Ch&xw5fy=x*<@wHU_bUxZ!)j)?X^kK@^u>0xaW9VCl4@w+,k8zMyq#UfF;?&x;?4@;Y[!t2yzMNaG.KG}p#x)d.:Hc_p#xToCAs@uZSeG=>bOWk{M)*AF^sW;d>bOWk{|0ap9Z`sa9Z@mV2(apR#gZNt3Z9k$W/|0aptoyN|74|Z9k$W/#IBi\"0!%gI#pZ9k$W/|0oslv2w$1hXxMT>yX1hXx>Pytk6Xgrfce)+~RsB1hXx>Pytk6D^g+#$>&x~4J%,L$>&xIPYZWAV4U>N64fCAH@M`!OhZjA7ZNpGg{/QurvsL?kW5HU2HhPy+>qZUP[kmi[Z0h#$/lScLEh9x>{G@lA|gC>L{Omhq9@jvr/)4WA/SK]7o$p:SjhCST=)aW1WA/SK]7o+h:SjhCS+g98kjlkW2zrm+h:Sk>r\"qWjlkW2zrmDi!c&>b=#{7zzyYpiIeU%9/X*\"zzyYD{Me_V7U6n>CWW$UZh7lWA)S%$nZlA_a/1hXx>PoZ{!/)TeQvG#*tZp&xIPYgOhDgU>e|!3a$>&xP{EgRJiTjACW@{)eKfo=L?BEM>gXyVcOWU+oZ_:SS\"n;]>0xi<`!hAyBQY4_Rfz,SP{j(1!{@g$6fejADEH:S(a!zV7ND\"F4b|BcgQ]_gp`W`|O%YbmlW~GE\"_JXoS\"fBV(~0*EqTkW7hie?ojLY#lZ,pD}.K*cp.@/!ng4{C;I0pKXGq>+qUF(L}~7AzRg@{C:vC5F=?vK*&<_+k\"_zg6JRzX}t|a2_Y6Fb~*UrO9/xb~ZiQ>6tw^*&cPzib&E%AO=sEn!_G(kGzJR1MIIVBzx]~_\"fZNP9c:bj,]oZuT9GA\"&.A4<0#2g`\"4&nHUCSSX*/$NYEO}_Q61MIlo/z#:Hhb2JB}*7>}QbJ~$GzRWMBYwN@L0AeT?TDNJJu.K9r/5|Ky?8Lo]cE\"{ec?M*`Z3y?|p3F7z8}Cg/,w=evO;j9ocl*Z?m00#h@y?E.JP?cLX7<31X&:EoJlZ:zUZcc]>DBpkK`4z81?E.&u\"}EC{@OpAzz5\":7<5fcggZ:>*j][zJvYt5N]7^)zLbR%0}]Ni)g%TF*u*\"rghP%Gk%LOQvUbbqJ?5v+^e0pl/&bnF9^S2[k6aX:*Qf;%;eHm#5x|]*O}E2616#f~i!jt<#olwffHzoBu}Mxr#Wu;J}K=hgVxEwLu?mswE=d=T/+lw\"f#=HW~1C6?lcibHM]jo~Y/8q~9E$CS{7ZbVrmk(E4@y*Ds0Nga|/HUnec~hM|_7&E>Y/8q~e{y)W.eUn+(5f(@lDd>Y/8q~xtO36^2^0=ff[D>Y/8q~vp$dwyi=5L>[h\"v;V*ErtGiBc{\"O^qS[%QIPjIqj_p+0K5Lv7rB?;#Pks~%RCa3a(qj[^L=M5W!Iod%5)`q0\"v;98_7aCl2nfyA<`3JNEDPsfV(2<300HiFdMd;Eq77BdJ4F$y~3y,c^E)qjW)$t6[c*c;M+$$,OF$y~3y`zKaQo6F7ju0PwxkxL4O_3[8`k,Pwu/IO`0L%?pshav[Un1!~wG/@(YlmF2~_F$y~3yeoMd:ModL3cqk]fad~|<1s>gO5.TDH}Sbtv?eb3gwsMo`RSfuUumzN6fU^V%(0&<8NwC3F$y~3yJfkS[*])h%OLoeKQ/2_dvD#$L8M(iHxS|Z/i$[Y_)Fcj0K\"sVFdMxS|Z/ib8qEco&;,1gABQZ3`3D(0?lVVF{E:G_;}0t,CQ4{^*ha,Qc[81WcVlUY9I\"SJQ5#co8Tl9mUE]^j&Uwl|E]^j&8x8bBtnKYI!}dEvC,#o*E;Oa[a(IHGYI:`};k;{<)]+tZ*ZT:R`^6twlZbHZf{l:}u9#T#!k=Zp{1^7Era&a:jsGy{0SL}H2#SL}*(G#gFd]Ea`OGRUz>y0y%Xb}v{F$u^/ib8XEL5rzQF$u^/iJcMSASv)>vdkcm8:5p{pP`6YMA5`nTwL5A/H^2pacmEbTivbXLF2VXFJm0+l#@#~.:.Tm!(qIDZ5/B{oRSR{bO63?F?YZ8Ga$A}MnJIDZ5/BKKd;9L#FH9.(P:zeZ90tJMCD!Hx=FdU*7Yrv+q/=XnA8of3+N?%Y[y|f@h}j5KtDesw=yQk;9}bk;9}Fylt/x9ov4,1!t:*JK;}{9O9=pYzk;9}Fylt/xDhodL.CHF!}OR51$(HF!}ry(ou;Do3FPK/|ZANAE$<@:v(ummR~f?U[P9ma2_GoP(_`fc0>K3z~HF!}ry(5^ET:3F*PlmBp+\"Y)?)a/.SPs{8rv_m5,ax5[{0bk7}YgJRnTOYt=C|E;o52$S[^Vn)WPg[6X]Es`#LJWZYpUkbr#<[)]Kv5b6CfRr+=IFc?:#1Gii|\"r8|DHNE75*S+ot@bEye+8N_]^{j%FS`La#WWoR,)GwCoHiB7sXLTD.=(UIhcDFq&(b4Ep^Lpe0*7#\"5!}.b:AzwTD(,jvDc=#_xA5@Ujzd*`dZ%>(Ut.WZw,1v{oXRI/^>s3%k>h,}_YdFq53y%/n_%5c;Ls?v;vKBw:^]z;z}1v`c&bFMlX!0VD_rn2vis;##Z6]?eK_b/Po,^&5@B?)Jpk.bBiSGCkk_<}Ihv=FEBIDFh`v)R0QI@bUJkXrGPi$*U\"f76b?!8XbSLj9m%Xu#,2FQ*Fi0\"(;zAT0zP;A}Fi+`.EJo]F(?b~P;A}Fiun;qA`NjvT9.13]AkA)l!G,^E2V~B7KHSfVN;]V8<@/U*7Yrv+q/=XnACmY?KzaCzO/Wr~VO!QJ|TX$+wq^>8m+6irsmTj?jSTA(68DmL\"M_Go6cn1wO^cMBrswn@y57#2YRNs2k;9}FyK5imd>^E8V@Hg5.EL{MX4z5A[wlJtk!Hq/m7VtI^(rYM,A@F:vJJxrr%IX;KMJ&Oa)}63UW~]T7|s=PXu*5Gq]afENN1!{Dr+;.SKV,#<$W*iy?]2nrBV@&Ha[~8B>Q0__zJCFRoiMkEgW5Z%]F,|d1lMNR5^`b{pT<11i(xfe8$Rk8bVKXOjEUrJHbAM0MwCpcMbGon2t8Y95GPaogPPvo3hcRwutu7qNvX02B9jUoJIae2Kv9Vu3GeGjrLcWBXWT6CuD7JZlNYh6GEwhrDgYLnOcBxYuBl45wZbJqnCrMrsN0IBAbZDMA6aV6pv7aRKdytjEkAGd1IJiXR2GSWg6iFz7rjsR20j8rjkkmpwgGopjmGYQ";cy[2]+=cy[11]-599,cy[16]+=cy[36]- -734,cy[28]+=cy[16]- -739,cy[46]+=cy[71]- -2188,cy[48]+=cy[69]- -483,cy[50]+=cy[53]- -44,cy[54]+=cy[75]-1046,cy[63]+=cy[32]- -5,cy[67]+=cy[41]-350,cy[76]+=cy[32]-1737;break;case 445:case-974:cy[2]+=cy[54]- -5317,cy[16]+=cy[56]- -1039,cy[28]+=cy[42]-2299,cy[46]+=cy[77]- -1321,cy[48]+=cy[49]- -266,cy[50]+=cy[36]-1512,cy[54]+=cy[63]- -5742,cy[63]+=cy[2]-1205,cy[67]+=cy[50]- -49,cy[76]+=cy[6]- -486;break;case-598:case cy[53]- -476:if(typeof module!==(1,cz["cg"]["b"])(cy[46]+5370,cy[48]+99)){cy[2]+=cy[30]-319,cy[16]+=cy[20]- -157,cy[28]+=cy[17]- -2340,cy[46]+=cy[22]- -635,cy[48]+=cy[22]-1497,cy[50]+=cy[53]-2416,cy[54]+=cy[55]- -65,cy[63]+=cy[70]-1025,cy[67]+=cy[76]-216,cy[76]+=cy[17]- -1203;break}else{cy[2]+=cy[75]-131,cy[16]+=cy[19]- -1473,cy[28]+=cy[14]- -2356,cy[46]+=cy[26]-1110,cy[48]+=cy[29]- -1161,cy[50]+=cy[9]-634,cy[54]+=cy[76]- -930,cy[63]+=cy[65]- -565,cy[67]+=cy[13]-525,cy[76]+=cy[47]-5335;break}case-345:[cz["ch"]["b"]]=cH;cz["ch"]["a"]=function(...cy){return cE([...cD(0,2),699,...cD(3,16),-3252,...cD(17,28),174,...cD(29,46),-99,525,74,-381,-633,...cD(51,54),437,...cD(55,63),774,...cD(64,67),-964,...cD(68,76),611,...cD(77,79)],{["ch"]:cz["ch"],["cg"]:cz["cg"],["ci"]:{}},cA,cy)};cz["ch"]["c"]=undefined;cz["ch"]["d"]=(1,cz["ch"]["a"])([...cD(cy[51]+-991,cy[30]+-840),711,...cD(cy[48]+-381,8),-706,...cD(9,cy[71]+549),-(cy[61]+1532),516,-2926,...cD(cy[48]+-363,31),738,...cD(32,cy[14]+-69),cy[25]+1364,-(cy[24]+-663),...cD(cy[34]+-888,cy[15]+395),cy[12]+566,-86,-(cy[6]+168),...cD(cy[68]+310,76),cy[69]+-514,...cD(77,cy[56]+992)]);if(cz["ch"]["c"]){cy[2]+=cy[59]-1064,cy[16]+=cy[78]-84,cy[28]+=cy[31]-513,cy[46]+=cy[20]-1743,cy[48]+=cy[28]-292,cy[50]+=cy[4]- -1124,cy[54]+=cy[67]- -3265,cy[63]+=cy[2]-917,cy[67]+=cy[71]- -2240,cy[76]+=cy[30]-1114;break}else{cy[2]+=cy[53]-1318,cy[16]+=cy[61]- -434,cy[28]+=cy[43]-2232,cy[46]+=cy[30]-1134,cy[48]+=cy[41]- -1266,cy[50]+=cy[49]- -2064,cy[54]+=cy[37]- -3392,cy[63]+=cy[32]-1256,cy[67]+=cy[40]- -960,cy[76]+=cy[16]- -911;break}case 920:case-551:case 207:return undefined;if(cy[18]!=cy[11]+1015){cy[2]+=cy[78]- -412,cy[16]+=cy[49]-458,cy[28]+=cy[58]- -593,cy[46]+=cy[53]- -972,cy[48]+=cy[64]-1674,cy[50]+=cy[76]- -1295,cy[54]+=cy[48]- -1215,cy[63]+=cy[77]- -1621,cy[67]+=cy[4]-3342,cy[76]+=cy[69]- -765;break}case 636:cz["cg"]["r"]=function(){function cy(cy,cH={["d"]:{}},cE){while(cC(cy)!==-973){switch(cC(cy)){case cy[6]- -1089:if(!(cy[49]==-381)){cy[6]+=cy[67]- -676,cy[10]+=cy[32]- -22,cy[15]+=cy[78]-1429,cy[16]+=cy[7]- -4916,cy[29]+=cy[52]-196,cy[42]+=cy[24]-2310,cy[56]+=cy[14]-5256,cy[75]+=cy[23]- -1975;break}cy[6]+=cy[9]-282,cy[10]+=cy[19]- -998,cy[15]+=cy[37]-657,cy[16]+=cy[54]- -3403,cy[29]+=cy[21]-727,cy[42]+=cy[32]-2427,cy[56]+=cy[6]-5504,cy[75]+=cy[34]- -304;break;case-336:if(cy[71]>-(cy[23]+1273)){cy[6]+=cy[18]-3691,cy[10]+=cy[46]- -1472,cy[15]+=cy[3]-1478,cy[16]+=cy[32]- -4178,cy[29]+=cy[15]-415,cy[42]+=cy[30]-1041,cy[56]+=cy[1]- -1198,cy[75]+=cy[33]-2645;break}if(!(cy[2]<-506)){cy[6]+=cy[79]- -729,cy[10]+=cy[16]- -4682,cy[15]+=cy[10]-3945,cy[16]+=cy[19]- -4900,cy[29]+=cy[16]-1482,cy[42]+=cy[38]- -1558,cy[56]+=cy[70]-282,cy[75]+=cy[63]-573;break}cy[6]+=cy[73]- -448,cy[10]+=cy[1]- -909,cy[15]+=cy[70]-528,cy[16]+=cy[26]- -159,cy[29]+=cy[25]-115,cy[42]+=cy[56]- -2174,cy[56]+=cy[31]- -1702,cy[75]+=cy[74]-2483;break;case cy[52]- -255:case-812:cH["d"]["a"]=new cz["cg"]["q"](cy[30]+-717);cH["d"]["b"]=cz["cg"]["p"][cB(cy[cy[65]+612]+212046,cy[14]+135,cy[40]+848)]||cz["cg"]["p"][cB(cy[48]+233025,cy[10]+219,12)];cy[6]+=cy[19]-3492,cy[10]+=cy[2]- -1779,cy[15]+=cy[37]- -3916,cy[16]+=cy[36]-623,cy[29]+=cy[15]- -427,cy[42]+=cy[50]-1539,cy[56]+=cy[72]- -669,cy[75]+=cy[26]-1031;break;case-594:case 493:if(cy[29]>-(cy[77]+1247)){;break}cy[6]+=cy[78]-230,cy[10]+=cy[45]-252,cy[15]+=cy[26]-2748,cy[16]+=cy[61]- -356,cy[29]+=cy[5]-279,cy[42]+=cy[24]- -840,cy[56]+=cy[36]- -2917,cy[75]+=cy[71]-598;break;case 421:case cy[74]-1027:cH["d"]["c"]=[];return cA=true,function(cy){function cE(cE,gN={["c"]:{}},cD){while(cC(cE)!==-215){switch(cC(cE)){case cE[64]-165:case 694:case-944:if(cE[cE[66]+437]==-(cE[40]+839)){cE[4]+=cE[54]- -195,cE[9]+=cE[68]- -194,cE[10]+=cE[56]-54,cE[18]+=cE[54]-476,cE[22]+=cE[71]- -53,cE[36]+=cE[56]- -870,cE[40]+=cE[21]-541,cE[61]+=cE[51]-803,cE[65]+=cE[9]- -233,cE[75]+=cE[54]-1155;break}if(cE[50]<-(cE[39]+-871)){cE[4]+=cE[3]-2184,cE[9]+=cE[47]-96,cE[10]+=cE[52]-347,cE[18]+=cE[23]- -1304,cE[22]+=cE[55]-648,cE[36]+=cE[7]-8,cE[40]+=cE[41]- -346,cE[61]+=cE[42]- -2479,cE[65]+=cE[70]-548,cE[75]+=cE[37]- -1135;break}cE[4]+=cE[20]-2598,cE[9]+=cE[60]-14,cE[10]+=cE[47]-4147,cE[18]+=cE[30]- -738,cE[22]+=cE[20]- -648,cE[36]+=cE[72]-231,cE[40]+=cE[42]- -828,cE[61]+=cE[62]-18,cE[65]+=cE[16]- -1192,cE[75]+=cE[79]- -525;break;case cE[25]-252:cE[4]+=cE[70]-341,cE[9]+=cE[28]- -854,cE[10]+=cE[23]- -1435,cE[18]+=cE[51]- -476,cE[22]+=cE[26]-333,cE[36]+=cE[10]- -159,cE[40]+=cE[5]- -1064,cE[61]+=cE[56]-478,cE[65]+=cE[79]- -1556,cE[75]+=cE[4]-3527;break;case 170:gN["c"]["c"]=cy[cB(cE[cE[20]+-443]+195414,cE[5]+-140,cE[62]+-890)];cH["d"]["c"][cB(cE[64]+394748,199,6)]=cE[17]+789;cE[4]+=cE[84]-926,cE[9]+=cE[61]- -527,cE[10]+=cE[33]-898,cE[18]+=cE[47]-821,cE[22]+=cE[48]-1162,cE[36]+=cE[33]-2220,cE[40]+=cE[0]-1891,cE[61]+=cE[12]- -679,cE[65]+=cE[84]-1725,cE[75]+=cE[26]- -3299;break;case cE[30]-196:cE[4]+=cE[54]-1079,cE[9]+=cE[53]- -332,cE[10]+=cE[65]-891,cE[18]+=cE[32]-1769,cE[22]+=cE[34]-1571,cE[36]+=cE[23]- -260,cE[40]+=cE[11]- -2391,cE[61]+=cE[5]- -171,cE[65]+=cE[51]-78,cE[75]+=cE[10]-507;break;case 77:case cE[40]- -2556:cE[4]+=cE[49]- -166,cE[9]+=cE[5]- -111,cE[10]+=cE[53]-262,cE[18]+=cE[16]- -1268,cE[22]+=cE[64]-1657,cE[36]+=cE[64]-207,cE[40]+=cE[52]- -2945,cE[61]+=cE[51]-1877,cE[65]+=cE[77]- -1813,cE[75]+=cE[39]-4731;break;case-289:case cE[57]- -495:case-321:cE[4]+=cE[9]- -1160,cE[9]+=cE[77]- -644,cE[10]+=cE[13]- -838,cE[18]+=cE[41]- -768,cE[22]+=cE[4]- -1067,cE[36]+=cE[61]-197,cE[40]+=cE[29]-1558,cE[61]+=cE[40]- -3376,cE[65]+=cE[55]-958,cE[75]+=cE[29]- -277;break;case cE[12]- -239:gN["c"]["a"]=undefined;gN["c"]["b"]=undefined;if(cE[66]<-967){cE[4]+=cE[31]-1861,cE[9]+=cE[62]-472,cE[10]+=cE[45]- -1839,cE[18]+=cE[77]- -903,cE[22]+=cE[58]-444,cE[36]+=cE[67]- -983,cE[40]+=cE[6]-941,cE[61]+=cE[28]- -222,cE[65]+=cE[80]-1606,cE[75]+=cE[66]- -311;break}cE[4]+=cE[15]-2052,cE[9]+=cE[82]- -321,cE[10]+=cE[67]- -660,cE[18]+=cE[83]-216,cE[22]+=cE[20]- -476,cE[36]+=cE[66]- -1299,cE[40]+=cE[7]- -1519,cE[61]+=cE[56]- -1242,cE[65]+=cE[27]- -1471,cE[75]+=cE[23]-2694;break;case-138:for(gN["c"]["d"]=0;gN["c"]["d"]-(cy[6]+4483))){cy[6]+=cy[34]- -3336,cy[10]+=cy[21]-5735,cy[15]+=cy[31]- -50,cy[16]+=cy[7]- -77,cy[29]+=cy[37]-92,cy[42]+=cy[6]- -1087,cy[56]+=cy[12]-303,cy[75]+=cy[42]-765;break}cy[6]+=cy[16]- -3368,cy[10]+=cy[44]-5504,cy[15]+=cy[57]- -455,cy[16]+=cy[54]-1436,cy[29]+=cy[36]-1867,cy[42]+=cy[68]- -2049,cy[56]+=cy[74]-782,cy[75]+=cy[35]- -302;break}}}var cA;var cH=cy([...cD(0,6),-3,...cD(7,10),38,...cD(11,15),-3094,640,...cD(17,29),-352,...cD(30,42),649,...cD(43,56),-929,...cD(57,75),-569,...cD(76,81)]);if(cA){return cH}}();cz["cg"]["s"]=[cy[22]+-626,cy[66]+401,cy[51]+-988,(cy[27]+697,cz["cg"]["b"])(cy[74]+-736,8),(cy[48]+477,cz["cg"]["b"])(157,cy[71]+542),undefined];cz["cg"]["t"]=[4,cy[3]+-588,0,cy[26]+-625,0,0,cy[42]+884,cy[47]+-523,cy[48]+479,cy[53]+86,cy[56]+914,cy[15]+344,cy[2]+557,cy[21]+-410,4,2,cy[16]+-237,cy[15]+383,cy[18]+-180,cy[24]+-768,cy[77]+843,3,cy[13]+341,cy[62]+-896,cy[56]+922,cy[39]+-883,cy[18]+-182,cy[26]+-625,5,cy[67]+1558,cy[55]+-267,cy[75]+-235,cy[61]+613,3,0,cy[24]+-799,cy[38]+203,cy[73]+741,0,cy[24]+-790,cy[53]+89,4,cy[49]+383,cy[69]+-584,3,cy[58]+-358,cy[7]+609,cy[12]+-357,3];cz["cg"]["u"]=0;cz["cg"]["v"]=cy[66]+404;cz["cg"]["w"]=cy[45]!=-626;cy[2]+=cy[13]- -296,cy[16]+=cy[37]-3230,cy[28]+=cy[38]- -155,cy[46]+=cy[8]- -823,cy[48]+=cy[52]- -1268,cy[50]+=cy[24]-1692,cy[54]+=cy[17]- -1029,cy[63]+=cy[31]- -798,cy[67]+=cy[12]- -1349,cy[76]+=cy[44]-363;break;case cy[44]- -695:cz["cg"]["C"]=new WeakMap;cz["cg"]["g"][(1,cz["cg"]["b"])(cy[47]+678,cy[9]+-961)][(1,cz["cg"]["b"])(cy[22]+585,cy[22]+-623)]=function(){function cy(cy){function cA(cA,cE={["C"]:{}},cD){while(cC(cA)!==-595){switch(cC(cA)){case cA[57]- -827:case-185:cE["C"]["a"]=cB(cA[33]+55834,1160,91);cE["C"]["b"]=""+(cy||"");if(cA[31]!=-331){cA[21]+=cA[12]-1965,cA[27]+=cA[28]-579,cA[32]+=cA[49]-58,cA[34]+=cA[60]- -243,cA[45]+=cA[32]- -265,cA[47]+=cA[33]-304,cA[60]+=cA[3]- -39,cA[66]+=cA[33]-1916,cA[68]+=cA[14]- -1195;break}cA[21]+=cA[33]- -678,cA[27]+=cA[62]-166,cA[32]+=cA[43]-1273,cA[34]+=cA[10]- -471,cA[45]+=cA[62]-1360,cA[47]+=cA[67]- -119,cA[60]+=cA[64]- -580,cA[66]+=cA[35]-2223,cA[68]+=cA[37]-216;break;case 924:case-330:case-882:return cH=true,(1,cz["cg"]["d"])(cE["C"]["d"]);if(cA[cA[31]+353]!=630){cA[21]+=cA[62]-2558,cA[27]+=cA[70]- -122,cA[32]+=cA[45]-1604,cA[34]+=cA[8]- -1267,cA[45]+=cA[66]-1170,cA[47]+=cA[30]-718,cA[60]+=cA[62]-1195,cA[66]+=cA[31]- -863,cA[68]+=cA[8]- -1424;break}cA[21]+=cA[30]-3034,cA[27]+=cA[9]-108,cA[32]+=cA[43]- -97,cA[34]+=cA[46]-965,cA[45]+=cA[32]-1136,cA[47]+=cA[10]-404,cA[60]+=cA[51]-716,cA[66]+=cA[65]- -1382,cA[68]+=cA[23]- -888;break;case cA[47]- -1287:cA[21]+=cA[2]- -1240,cA[27]+=cA[75]-1877,cA[32]+=cA[35]- -588,cA[34]+=cA[57]-241,cA[45]+=cA[11]- -1839,cA[47]+=cA[2]- -2037,cA[60]+=cA[8]- -1349,cA[66]+=cA[70]-2131,cA[68]+=cA[32]-123;break;case-699:cA[21]+=cA[15]- -2042,cA[27]+=cA[16]- -378,cA[32]+=cA[20]-507,cA[34]+=cA[74]-156,cA[45]+=cA[30]-984,cA[47]+=cA[0]-397,cA[60]+=cA[37]- -1045,cA[66]+=cA[48]-1110,cA[68]+=cA[65]- -48;break;case 142:cE["C"]["g"]=-(cA[69]+-583);for(cE["C"]["h"]=cA[51]+-991;cE["C"]["h"]cA[53]+174?13:cA[1]+501;do{cE["C"]["d"].push(cE["C"]["e"]&255);cE["C"]["e"]>>=8;cE["C"]["f"]-=8}while(cE["C"]["f"]>cA[58]+-356);cE["C"]["g"]=-(cA[60]+-194)}}if(cE["C"]["g"]>-1){cA[21]+=cA[71]- -2283,cA[27]+=cA[21]-251,cA[32]+=cA[24]- -368,cA[34]+=cA[25]- -641,cA[45]+=cA[34]-4304,cA[47]+=cA[29]- -317,cA[60]+=cA[22]-634,cA[66]+=cA[5]-440,cA[68]+=cA[13]- -584;break}else{cA[21]+=cA[13]- -2001,cA[27]+=cA[72]-446,cA[32]+=cA[22]-597,cA[34]+=cA[22]-1366,cA[45]+=cA[35]- -1673,cA[47]+=cA[31]- -204,cA[60]+=cA[19]- -359,cA[66]+=cA[24]-1331,cA[68]+=cA[69]-1477;break}if(cA[cA[11]+835]==-(cA[73]+1069)){cA[21]+=cA[65]- -2363,cA[27]+=cA[3]-55,cA[32]+=cA[40]- -2002,cA[34]+=cA[44]-46,cA[45]+=cA[29]-3206,cA[47]+=cA[59]-263,cA[60]+=cA[50]- -12,cA[66]+=cA[41]- -670,cA[68]+=cA[32]-213;break}case 351:case 399:case-74:cE["C"]["d"].push((cE["C"]["e"]|cE["C"]["g"]<88?13:cz["cv"]["a"][93]+231;do{cz["cv"]["b"]["ce"]["d"].push(cz["cv"]["b"]["ce"]["e"]&cz["cv"]["a"][31]+-100);cz["cv"]["b"]["ce"]["e"]>>=8;cz["cv"]["b"]["ce"]["f"]-=cz["cv"]["a"][46]+111}while(cz["cv"]["b"]["ce"]["f"]>cz["cv"]["a"][48]+-274);cz["cv"]["b"]["ce"]["g"]=-(cz["cv"]["a"][88]+-19)}}if(cz["cv"]["b"]["ce"]["g"]>-(cz["cv"]["a"][92]+-755)){cz["cv"]["a"][cy[27]+714]+=cz["cv"]["a"][cy[60]+-897]-(cy[77]+1874),cz["cv"]["a"][cy[11]+857]+=cz["cv"]["a"][51]-(cy[70]+737),cz["cv"]["a"][cy[9]+-943]+=cz["cv"]["a"][cy[44]+-160]- -1071,cz["cv"]["a"][cy[62]+-865]+=cz["cv"]["a"][16]- -(cy[52]+458),cz["cv"]["a"][cy[30]+-813]+=cz["cv"]["a"][cy[32]+-859]- -(cy[60]+190),cz["cv"]["a"][67]+=cz["cv"]["a"][81]-(cy[2]+1278),cz["cv"]["a"][70]+=cz["cv"]["a"][92]-(cy[48]+1473),cz["cv"]["a"][cy[11]+903]+=cz["cv"]["a"][cy[23]+807]-1585,cz["cv"]["a"][cy[40]+912]+=cz["cv"]["a"][cy[37]+930]- -1186,cz["cv"]["a"][90]+=cz["cv"]["a"][cy[62]+-841]- -(cy[44]+1175),cz["cv"]["a"][cy[71]+625]+=cz["cv"]["a"][35]- -1268;break}else{cz["cv"]["a"][cy[71]+546]+=cz["cv"]["a"][cy[49]+463]-(cy[71]+1718),cz["cv"]["a"][26]+=cz["cv"]["a"][76]- -(cy[59]+237),cz["cv"]["a"][29]+=cz["cv"]["a"][46]-(cy[33]+-366),cz["cv"]["a"][31]+=cz["cv"]["a"][cy[62]+-814]- -(cy[3]+-366),cz["cv"]["a"][cy[25]+517]+=cz["cv"]["a"][94]- -1036,cz["cv"]["a"][67]+=cz["cv"]["a"][78]-(cy[14]+381),cz["cv"]["a"][cy[52]+168]+=cz["cv"]["a"][cy[72]+35]- -(cy[33]+-339),cz["cv"]["a"][cy[73]+809]+=cz["cv"]["a"][cy[69]+-507]-(cy[6]+2560),cz["cv"]["a"][cy[5]+-254]+=cz["cv"]["a"][93]- -269,cz["cv"]["a"][90]+=cz["cv"]["a"][cy[14]+-68]- -(cy[49]+2589),cz["cv"]["a"][97]+=cz["cv"]["a"][32]- -(cy[64]+-544);break}if(cz["cv"]["a"][cz["cv"]["a"][20]+-449]<-(cz["cv"]["a"][95]+1676)){cz["cv"]["a"][cy[39]+-869]+=cz["cv"]["a"][cy[4]+689]-1535,cz["cv"]["a"][26]+=cz["cv"]["a"][87]-470,cz["cv"]["a"][29]+=cz["cv"]["a"][cy[11]+855]-689,cz["cv"]["a"][31]+=cz["cv"]["a"][67]-(cy[9]+-774),cz["cv"]["a"][cy[64]+-889]+=cz["cv"]["a"][cy[71]+563]- -1286,cz["cv"]["a"][cy[0]+-589]+=cz["cv"]["a"][85]-(cy[13]+790),cz["cv"]["a"][cy[76]+971]+=cz["cv"]["a"][7]-572,cz["cv"]["a"][cy[49]+453]+=cz["cv"]["a"][cy[15]+358]- -(cy[2]+1266),cz["cv"]["a"][cy[52]+175]+=cz["cv"]["a"][9]-(cy[16]+-417),cz["cv"]["a"][cy[66]+489]+=cz["cv"]["a"][cy[36]+-855]- -570,cz["cv"]["a"][97]+=cz["cv"]["a"][8]- -(cy[72]+486);break}case 949:case-(cy[35]+230):case-548:return cz["cu"]["c"]=cy[57]!=3,(1,cz["cg"]["d"])(cz["cv"]["b"]["ce"]["d"]);cz["cv"]["a"][18]+=cz["cv"]["a"][64]-1132,cz["cv"]["a"][cy[31]+357]+=cz["cv"]["a"][cy[18]+-125]-(cy[62]+539),cz["cv"]["a"][29]+=cz["cv"]["a"][cy[51]+-952]-1394,cz["cv"]["a"][cy[13]+370]+=cz["cv"]["a"][49]-(cy[57]+1249),cz["cv"]["a"][cy[3]+-557]+=cz["cv"]["a"][38]- -2533,cz["cv"]["a"][67]+=cz["cv"]["a"][91]- -(cy[16]+614),cz["cv"]["a"][cy[49]+451]+=cz["cv"]["a"][cy[62]+-844]-(cy[51]+-320),cz["cv"]["a"][cy[40]+907]+=cz["cv"]["a"][cy[52]+175]- -4147,cz["cv"]["a"][cy[12]+-325]+=cz["cv"]["a"][50]- -(cy[44]+272),cz["cv"]["a"][90]+=cz["cv"]["a"][75]-(cy[3]+654),cz["cv"]["a"][cy[17]+886]+=cz["cv"]["a"][38]-1173;break;case 118:case-212:case cz["cv"]["a"][90]- -2843:cz["cv"]["a"][18]+=cz["cv"]["a"][54]-1365,cz["cv"]["a"][cy[58]+-337]+=cz["cv"]["a"][cy[38]+223]-(cy[4]+1544),cz["cv"]["a"][cy[17]+818]+=cz["cv"]["a"][32]- -(cy[47]+-17),cz["cv"]["a"][cy[44]+-171]+=cz["cv"]["a"][28]-(cy[28]+947),cz["cv"]["a"][32]+=cz["cv"]["a"][cy[6]+14]- -(cy[72]+72),cz["cv"]["a"][cy[25]+552]+=cz["cv"]["a"][21]-(cy[37]+2397),cz["cv"]["a"][cy[9]+-902]+=cz["cv"]["a"][cy[59]+-353]-(cy[77]+2209),cz["cv"]["a"][72]+=cz["cv"]["a"][cy[20]+-418]-358,cz["cv"]["a"][77]+=cz["cv"]["a"][cy[72]+19]-(cy[57]+1926),cz["cv"]["a"][cy[33]+-654]+=cz["cv"]["a"][cy[30]+-833]- -3782,cz["cv"]["a"][cy[7]+706]+=cz["cv"]["a"][cy[36]+-782]- -(cy[68]+550);break;case 661:case cy[67]+222:cz["cv"]["a"][18]+=cz["cv"]["a"][cy[70]+-264]- -(cy[31]+6000),cz["cv"]["a"][26]+=cz["cv"]["a"][77]-1667,cz["cv"]["a"][29]+=cz["cv"]["a"][cy[55]+-250]-29,cz["cv"]["a"][cy[30]+-814]+=cz["cv"]["a"][22]-(cy[19]+189),cz["cv"]["a"][cy[9]+-940]+=cz["cv"]["a"][cy[16]+-752]- -(cy[6]+4493),cz["cv"]["a"][67]+=cz["cv"]["a"][cy[61]+657]-(cy[25]+1700),cz["cv"]["a"][cy[5]+-261]+=cz["cv"]["a"][46]- -(cy[75]+312),cz["cv"]["a"][cy[29]+205]+=cz["cv"]["a"][48]-610,cz["cv"]["a"][77]+=cz["cv"]["a"][14]-(cy[19]+2061),cz["cv"]["a"][cy[63]+-744]+=cz["cv"]["a"][cy[37]+898]-(cy[26]+1492),cz["cv"]["a"][cy[74]+-784]+=cz["cv"]["a"][cy[67]+-186]-(cy[54]+4456);break;case-436:cz["cv"]["b"]["ce"]["a"]=cB(cz["cv"]["a"][56]+587662,13714,91);cz["cv"]["b"]["ce"]["b"]=""+(cz["cu"]["b"]||"");cz["cv"]["b"]["ce"]["c"]=cz["cv"]["b"]["ce"]["b"].length;cz["cv"]["b"]["ce"]["d"]=[];cz["cv"]["a"][cy[30]+-827]+=cz["cv"]["a"][36]- -70,cz["cv"]["a"][cy[14]+-79]+=cz["cv"]["a"][cy[46]+-251]- -(cy[61]+2468),cz["cv"]["a"][29]+=cz["cv"]["a"][37]-229,cz["cv"]["a"][cy[33]+-713]+=cz["cv"]["a"][cy[40]+902]- -1253,cz["cv"]["a"][32]+=cz["cv"]["a"][cy[56]+914]-(cy[6]+284),cz["cv"]["a"][cy[33]+-677]+=cz["cv"]["a"][75]-(cy[60]+-723),cz["cv"]["a"][cy[46]+-270]+=cz["cv"]["a"][cy[56]+999]- -(cy[64]+272),cz["cv"]["a"][72]+=cz["cv"]["a"][cy[60]+-940]-666,cz["cv"]["a"][cy[9]+-895]+=cz["cv"]["a"][cy[30]+-789]- -(cy[30]+912),cz["cv"]["a"][cy[20]+-413]+=cz["cv"]["a"][83]-(cy[51]+2082),cz["cv"]["a"][97]+=cz["cv"]["a"][12]- -958;break;case cz["cv"]["a"][cy[14]+-102]-1505:cz["cv"]["a"][cy[40]+853]+=cz["cv"]["a"][cy[71]+569]-124,cz["cv"]["a"][26]+=cz["cv"]["a"][12]-396,cz["cv"]["a"][29]+=cz["cv"]["a"][cy[59]+-371]-1297,cz["cv"]["a"][cy[65]+639]+=cz["cv"]["a"][cy[58]+-276]- -(cy[60]+2263),cz["cv"]["a"][32]+=cz["cv"]["a"][cy[30]+-794]- -2191,cz["cv"]["a"][cy[20]+-436]+=cz["cv"]["a"][cy[45]+701]-(cy[2]+1192),cz["cv"]["a"][70]+=cz["cv"]["a"][cy[6]+-13]-(cy[70]+956),cz["cv"]["a"][cy[10]+60]+=cz["cv"]["a"][cy[28]+885]- -1741,cz["cv"]["a"][cy[64]+-844]+=cz["cv"]["a"][cy[70]+-245]-2038,cz["cv"]["a"][90]+=cz["cv"]["a"][14]-154,cz["cv"]["a"][cy[66]+496]+=cz["cv"]["a"][cy[65]+655]-(cy[8]+1847);break;case 449:case cy[14]+-19:case cz["cv"]["a"][cy[28]+897]-(cy[11]+1296):return cz["cu"]["c"]=cy[53]==-86,(1,cz["cg"]["d"])(cz["cv"]["b"]["ce"]["d"]);cz["cv"]["a"][cy[75]+-219]+=cz["cv"]["a"][cy[40]+860]-(cy[35]+1127),cz["cv"]["a"][26]+=cz["cv"]["a"][cy[37]+925]- -4363,cz["cv"]["a"][29]+=cz["cv"]["a"][cy[11]+881]-(cy[25]+1980),cz["cv"]["a"][cy[27]+727]+=cz["cv"]["a"][35]- -(cy[67]+175),cz["cv"]["a"][cy[74]+-849]+=cz["cv"]["a"][cy[20]+-437]- -(cy[36]+3619),cz["cv"]["a"][cy[73]+804]+=cz["cv"]["a"][37]- -(cy[37]+1350),cz["cv"]["a"][70]+=cz["cv"]["a"][64]-(cy[57]+2324),cz["cv"]["a"][cy[46]+-268]+=cz["cv"]["a"][67]-1306,cz["cv"]["a"][77]+=cz["cv"]["a"][65]-390,cz["cv"]["a"][90]+=cz["cv"]["a"][cy[22]+-535]- -(cy[11]+1361),cz["cv"]["a"][cy[30]+-748]+=cz["cv"]["a"][41]- -(cy[6]+170);break;case-(cy[16]+-604):case-(cy[9]+-725):case cz["cv"]["a"][cy[56]+931]- -321:cz["cv"]["b"]["ce"]["e"]=cy[45]+626;cz["cv"]["b"]["ce"]["f"]=cz["cv"]["a"][88]+-20;if(!(cz["cv"]["a"][cz["cv"]["a"][95]+838]!=cy[77]+1729)){;break}cz["cv"]["a"][cy[18]+-166]+=cz["cv"]["a"][cy[41]+816]- -(cy[73]+1424),cz["cv"]["a"][26]+=cz["cv"]["a"][64]-(cy[71]+1981),cz["cv"]["a"][cy[31]+360]+=cz["cv"]["a"][23]- -(cy[62]+866),cz["cv"]["a"][cy[59]+-416]+=cz["cv"]["a"][22]-(cy[57]+1769),cz["cv"]["a"][32]+=cz["cv"]["a"][15]-(cy[73]+1747),cz["cv"]["a"][cy[27]+763]+=cz["cv"]["a"][60]- -152,cz["cv"]["a"][cy[72]+42]+=cz["cv"]["a"][60]-1014,cz["cv"]["a"][72]+=cz["cv"]["a"][89]- -(cy[64]+915),cz["cv"]["a"][77]+=cz["cv"]["a"][cy[72]+46]-2282,cz["cv"]["a"][cy[33]+-654]+=cz["cv"]["a"][cy[58]+-308]- -959,cz["cv"]["a"][cy[17]+886]+=cz["cv"]["a"][19]-(cy[10]+1271);break;case cz["cv"]["a"][19]-545:case-(cy[58]+240):case cy[30]+-40:if(!(cz["cv"]["a"][cz["cv"]["a"][26]+-624]==-(cy[8]+617))){cz["cv"]["a"][cy[61]+628]+=cz["cv"]["a"][cy[51]+-906]-250,cz["cv"]["a"][26]+=cz["cv"]["a"][cy[10]+61]-659,cz["cv"]["a"][cy[13]+368]+=cz["cv"]["a"][cy[54]+2938]- -4634,cz["cv"]["a"][cy[58]+-332]+=cz["cv"]["a"][27]- -(cy[76]+1345),cz["cv"]["a"][32]+=cz["cv"]["a"][55]-244,cz["cv"]["a"][67]+=cz["cv"]["a"][55]-(cy[12]+107),cz["cv"]["a"][70]+=cz["cv"]["a"][76]-(cy[40]+1140),cz["cv"]["a"][72]+=cz["cv"]["a"][3]-613,cz["cv"]["a"][cy[55]+-194]+=cz["cv"]["a"][84]-220,cz["cv"]["a"][cy[40]+925]+=cz["cv"]["a"][cy[9]+-915]- -963,cz["cv"]["a"][cy[3]+-492]+=cz["cv"]["a"][49]-(cy[67]+858);break}cz["cv"]["a"][cy[51]+-973]+=cz["cv"]["a"][cy[30]+-759]- -(cy[73]+791),cz["cv"]["a"][26]+=cz["cv"]["a"][cy[10]+53]- -(cy[16]+-753),cz["cv"]["a"][cy[23]+774]+=cz["cv"]["a"][88]- -(cy[15]+4186),cz["cv"]["a"][cy[12]+-371]+=cz["cv"]["a"][29]-(cy[57]+1144),cz["cv"]["a"][cy[36]+-845]+=cz["cv"]["a"][cy[56]+914]-(cy[29]+1802),cz["cv"]["a"][cy[16]+-767]+=cz["cv"]["a"][82]- -(cy[76]+1729),cz["cv"]["a"][70]+=cz["cv"]["a"][59]- -520,cz["cv"]["a"][72]+=cz["cv"]["a"][3]-779,cz["cv"]["a"][77]+=cz["cv"]["a"][cy[44]+-173]-272,cz["cv"]["a"][90]+=cz["cv"]["a"][cy[64]+-890]-1328,cz["cv"]["a"][97]+=cz["cv"]["a"][18]-(cy[54]+4738);break;case cz["cv"]["a"][cy[15]+348]- -(cy[32]+-580):case cy[33]+-474:return cz["cu"]["c"]=cy[45]!=-880,(1,cz["cg"]["d"])(cz["cv"]["b"]["ce"]["d"]);cz["cv"]["a"][18]+=cz["cv"]["a"][3]-1874,cz["cv"]["a"][cy[18]+-158]+=cz["cv"]["a"][96]-1291,cz["cv"]["a"][29]+=cz["cv"]["a"][48]-(cy[68]+815),cz["cv"]["a"][cy[33]+-713]+=cz["cv"]["a"][cy[0]+-611]- -636,cz["cv"]["a"][cy[74]+-849]+=cz["cv"]["a"][cy[32]+-825]- -(cy[61]+4793),cz["cv"]["a"][67]+=cz["cv"]["a"][cy[10]+6]- -(cy[56]+2417),cz["cv"]["a"][70]+=cz["cv"]["a"][cy[17]+877]-185,cz["cv"]["a"][72]+=cz["cv"]["a"][64]-(cy[77]+1726),cz["cv"]["a"][77]+=cz["cv"]["a"][47]-1145,cz["cv"]["a"][90]+=cz["cv"]["a"][21]-360,cz["cv"]["a"][cy[20]+-406]+=cz["cv"]["a"][66]-(cy[14]+951);break;case-(cy[65]+1492):case-803:case-747:cz["cv"]["b"]["ce"]["d"].push((cz["cv"]["b"]["ce"]["e"]|cz["cv"]["b"]["ce"]["g"]<>>3)+n|0;l=l^l>>>15|0;l=l+(l<<11)|0;m=m^(l<<4)+(l>>>9)+n|0;m=m+(m<<6)|0;m=m^m>>>13|0;n=n+2135587861|0}l=l^m|0;l=l+(l<<3)|0;l=l^l>>>11|0;l=l+(l<<15)|0;m=m^m>>>13|0;m=m+(m<<7)|0;m=m^m>>>17|0;return(l>>>0)*1048576+(m>>>12)}var k=[656,-487,-892,589,-665,331,59,-609,-531,972,12,-831,402,-339,105,-343,-613,-789,184,-60,503,433,630,-745,799,-485,628,-696,127,-133,845,-331,916,744,926,-179,877,-898,-199,887,-835,-779,-879,846,202,-626,-103,525,281,-381,-16,991,-98,-86,904,271,-911,-736,363,447,982,-610,896,-925,921,-608,-399,14,-255,584,296,-528,28,-737,881,237,91,-843,-126,-684,811,-485,46,643,627,-661,-229,-399,20,-457,-523,-542,756,-217,683,-790,692,994,886,448,-627,847],l="Eu(]r`%85@&RpDCSVm'=P9rKMu>*RZodzK8ky,_Vy@/eEud6-qo]j[jaBnqxW)7RSwX]\"mA_MEEzR/@#EAYK0ZvV:>gn` o:I,0:~I,?;k~4=.oXbXTcuQ44bm-qb#eV5E5)nV~*`}U2!f3qWY7&xSIxwegiDSZOY>/M3kusTPl@-h[]Ige))(~O5vkG+6H$O/Wt36Hwv>k$w.z-<)FOM)Du4h(U*q4W1cej0n8;yiD$Y/nLDgZa1TB.fUd7*M.0WG3[2t4GC*.s[A~DvO_h:$F9y);@9#]gn;!Q{W7<9(E%kf5Z3@X~HF;nFuF[DIj}k4o|ITvGIIIi]~EA:II/{LzFXwVoo}1o5/}T+||w%}1r3+\\K+j508'A'\"6JLbh1%t:f4&\"FdJe>dti/*+o||B>wTo|,SBB6jXFbSm79I4+xwC=y1hF;6}m^=%X{f d~.MVL&E2li 1:-SrE~(?XWt>rr5:D+|Le&`ugG[y`vSuyO#]9ReJE~#RP=ry?a5Q(~9Mexr3#~XYO_,fdWDs;o>HM^@e~N/<2/Y]?otPbnAV]R)z6yliL{7]Hcr1E;!CyFk5@EGz8;-R7PO=Q{: EX&Z\"&rE.c[\"";function m(j,k,m){for(var n="",o=0;o>>13)%95+95)%95;var q=l["charCodeAt"](k+o)-32;var r=((q-p)%95+95)%95;n+=String.fromCharCode(r+32)}return n}function n(j){for(var k=0,l=0;l202){j[5]+=j[18]- -1130,j[15]+=j[63]- -1770,j[18]+=j[20]- -552,j[26]+=j[13]- -280,j[29]+=j[59]-2154,j[46]+=j[84]- -293,j[54]+=j[56]-141,j[66]+=j[58]- -308,j[67]+=j[53]- -1190,j[71]+=j[63]- -641,j[83]+=j[78]-708;break}j[5]+=j[31]- -347,j[15]+=j[52]- -305,j[18]+=j[24]- -651,j[26]+=j[65]- -497,j[29]+=j[20]-937,j[46]+=j[59]- -815,j[54]+=j[13]-1062,j[66]+=j[13]- -200,j[67]+=j[14]- -734,j[71]+=j[67]-318,j[83]+=j[71]-1077;break;case j[84]-1146:[k["e"]["b"]]=s;k["e"]["a"]=function(...j){return p([...o(0,5),-127,...o(6,15),3290,...o(16,18),499,...o(19,26),-924,...o(27,29),-915,...o(30,46),-952,...o(47,54),414,...o(55,66),120,-878,...o(68,71),484,...o(72,83),-984,...o(84,89)],{["e"]:k["e"],["c"]:k["c"],["f"]:{}},l,j)};k["e"]["c"]=undefined;k["e"]["d"]=(j[81]+486,k["e"]["a"])([656,-(j[23]+1085),j[35]+246,...o(j[86]+232,j[46]+113),-(j[34]+-386),...o(8,22),j[77]+1289,...o(j[82]+-23,j[35]+212),-(j[83]+2032),...o(j[10]+22,j[16]+650),j[46]+608,...o(38,j[61]+650),245,...o(j[42]+920,57),-(j[70]+576),...o(j[71]+371,j[58]+-303),-747,...o(j[77]+904,j[34]+-850),j[35]+519,...o(j[28]+-50,j[70]+-211)]);if(k["e"]["c"]){j[5]+=j[61]-588,j[15]+=j[6]- -93,j[18]+=j[6]-28,j[26]+=j[71]- -107,j[29]+=j[25]- -41,j[46]+=j[36]-1416,j[54]+=j[11]- -2300,j[66]+=j[31]- -330,j[67]+=j[42]- -929,j[71]+=j[24]-155,j[83]+=j[7]- -587;break}else{j[5]+=j[1]-523,j[15]+=j[6]- -1251,j[18]+=j[47]-109,j[26]+=j[25]- -379,j[29]+=j[32]-1125,j[46]+=j[5]- -1278,j[54]+=j[39]-254,j[66]+=j[11]- -475,j[67]+=j[32]-213,j[71]+=j[78]-268,j[83]+=j[0]-875;break}case j[78]- -201:case 36:return undefined;case-968:case 469:j[5]+=j[82]- -442,j[15]+=j[84]- -1677,j[18]+=j[26]-1233,j[26]+=j[9]-2508,j[29]+=j[24]-1806,j[46]+=j[0]-1323,j[54]+=j[68]- -1374,j[66]+=j[41]- -1006,j[67]+=j[80]-795,j[71]+=j[31]- -253,j[83]+=j[75]-1354;break;case 554:case 377:case-120:k["c"]["e"][m(j[65]+366162,947,9)][m(j[22]+544302,961,14)]=function(j,l){var s=this["_regs"][j+k["c"]["o"]["REG_BASE"]]+l;var m=this["_openUpvalues"]||(this["_openUpvalues"]=[]);return m[s]||(m[s]=new k["c"]["c"](this["_regs"],s))};k["c"]["e"][m(j[6]+925028,978,9)][m(j[65]+596369,991,9)]=function(j,l){var s=j??this["_operand"]();var m=l??this["_operand"]();var n=this["constants"][s];if(!m){return n}if(typeof n==="number"){return n^m}if(typeof n!=="string"){return n}var o=(1,k["c"]["a"])(n);var p="";var B=m;for(var C=0;C>>13)&65535;var E=o[C*2]|o[C*2+1]<<8;p+=String["fromCharCode"](E^D)}return p};j[5]+=j[73]- -787,j[15]+=j[17]-987,j[18]+=j[4]- -2116,j[26]+=j[38]-630,j[29]+=j[17]- -1554,j[46]+=j[76]-2509,j[54]+=j[80]-2264,j[66]+=j[81]- -2254,j[67]+=j[11]-21,j[71]+=j[64]- -194,j[83]+=j[22]- -603;break;case 194:case j[50]-533:k["c"]["r"][m(j[15]+446150,1118,6)]=module;k["c"]["r"][m(j[78]+818646,1127,7)]=typeof exports!==m(j[86]+918958,1136,9)?exports:undefined;if(j[49]>786){j[5]+=j[39]- -708,j[15]+=j[30]-1063,j[18]+=j[69]-2052,j[26]+=j[59]-861,j[29]+=j[39]-118,j[46]+=j[66]-1709,j[54]+=j[34]- -3084,j[66]+=j[18]-148,j[67]+=j[18]- -623,j[71]+=j[16]-595,j[83]+=j[48]-260;break}j[5]+=j[35]- -2227,j[15]+=j[22]-1164,j[18]+=j[0]-745,j[26]+=j[67]- -811,j[29]+=j[31]- -24,j[46]+=j[71]-1419,j[54]+=j[53]- -1378,j[66]+=j[88]-629,j[67]+=j[61]- -1792,j[71]+=j[21]-199,j[83]+=j[33]-3318;break;case-722:k["c"]["q"]=new WeakMap;k["c"]["c"][m(j[78]+351009,819,9)][m(j[12]+525390,832,5)]=function(){return this["_closed"]?this["_value"]:this["_regs"][this["_absSlot"]]};k["c"]["c"][m(j[40]+67994,840,9)][m(j[16]+909492,855,6)]=function(j){if(this["_closed"]){this["_value"]=j}else{this["_regs"][this["_absSlot"]]=j}};k["c"]["c"][m(j[51]+922878,869,9)][m(j[64]+968641,883,6)]=function(){this["_value"]=this["_regs"][this["_absSlot"]];this["_closed"]=true};j[5]+=j[77]- -298,j[15]+=j[38]- -1203,j[18]+=j[27]- -679,j[26]+=j[38]-4,j[29]+=j[22]-91,j[46]+=j[75]- -795,j[54]+=j[5]-1258,j[66]+=j[60]-1021,j[67]+=j[81]- -2359,j[71]+=j[18]-852,j[83]+=j[45]-474;break;case-103:case 700:case-583:return k["e"]["d"];j[5]+=j[31]- -519,j[15]+=j[12]- -756,j[18]+=j[54]-428,j[26]+=j[1]- -587,j[29]+=j[87]- -634,j[46]+=j[74]- -47,j[54]+=j[15]-1665,j[66]+=j[10]-367,j[67]+=j[78]- -779,j[71]+=j[40]-203,j[83]+=j[50]-181;break;case j[74]-1686:if(j[j[31]+381]!=-16){j[5]+=j[14]-536,j[15]+=j[50]- -1816,j[18]+=j[71]- -640,j[26]+=j[18]-836,j[29]+=j[75]- -1299,j[46]+=j[33]-1400,j[54]+=j[23]- -122,j[66]+=j[75]-302,j[67]+=j[48]-640,j[71]+=j[76]-727,j[83]+=j[0]- -379;break}j[5]+=j[88]- -401,j[15]+=j[23]- -1430,j[18]+=j[20]-1004,j[26]+=j[37]- -723,j[29]+=j[10]- -1756,j[46]+=j[27]-408,j[54]+=j[46]- -72,j[66]+=j[76]-1285,j[67]+=j[60]-1560,j[71]+=j[52]- -40,j[83]+=j[20]- -242;break;case-675:k["c"]["r"]=globalThis;if(typeof window!==m(j[63]+599105,1062,9)){j[5]+=j[79]- -975,j[15]+=j[86]- -5420,j[18]+=j[46]-1903,j[26]+=j[17]- -27,j[29]+=j[75]-190,j[46]+=j[25]-536,j[54]+=j[8]- -372,j[66]+=j[58]-1423,j[67]+=j[88]-1066,j[71]+=j[79]-278,j[83]+=j[80]-497;break}else{j[5]+=j[25]- -792,j[15]+=j[30]- -4553,j[18]+=j[77]- -1246,j[26]+=j[10]-885,j[29]+=j[16]- -226,j[46]+=j[10]- -229,j[54]+=j[5]-1539,j[66]+=j[69]-1783,j[67]+=j[70]-503,j[71]+=j[66]- -127,j[83]+=j[52]-470;break}case j[44]- -169:k["c"]["e"]=function j(l,s,m){this["bytecode"]=l;this["constants"]=s;this["globals"]=m;this["_openUpvalues"]=null;this["_regs"]=[];this["_regsTop"]=k["c"]["f"];this["_f"]=0};k["c"]["d"]=function j(k){this["fn"]=k;this["upvalues"]=[];this["prototype"]={}};k["c"]["c"]=function j(k,l){this["_regs"]=k;this["_absSlot"]=l;this["_closed"]=false;this["_value"]=undefined};k["c"]["b"]=function(...j){return p([...o(0,5),121,...o(6,15),-481,...o(16,18),-307,...o(19,26),-301,...o(27,29),815,...o(30,46),-106,...o(47,54),-656,...o(55,66),859,-681,...o(68,71),-313,...o(72,83),961,...o(84,89)],{["c"]:k["c"],["e"]:{}},l,j)};k["c"]["a"]=function(...j){return p([...o(0,5),475,...o(6,15),-131,...o(16,18),-307,...o(19,26),324,...o(27,29),254,...o(30,46),257,...o(47,54),6,...o(55,66),380,-707,...o(68,71),-162,...o(72,83),-16,...o(84,89)],{["c"]:k["c"],["d"]:{}},l,j)};k["c"]["g"]=[j[83]+-274,j[72]+-26,j[59]+-444,m(j[46]+853077,21,j[8]+537),m(j[3]+805484,j[5]+-200,11),undefined];k["c"]["h"]=[j[32]+-912,j[63]+926,j[42]+879,j[86]+232,j[75]+-237,j[69]+-584,j[25]+490,2,j[4]+668,j[22]+-630,j[77]+846,j[39]+-886,j[60]+-982,j[55]+-248,4,j[63]+927,j[38]+202,40,j[44]+-198,j[41]+810,j[64]+-921,j[72]+-25,j[63]+927,j[10]+-12,j[80]+-800,j[39]+-883,j[19]+62,j[38]+202,j[61]+615,2,j[58]+-359,j[2]+894,j[25]+488,j[70]+-293,j[20]+-503,j[69]+-584,4,j[23]+749,0,9,3,j[72]+-24,j[27]+698,j[73]+737,j[15]+429,j[0]+-651,j[70]+-296,j[21]+-388,j[16]+616];j[5]+=j[37]- -1321,j[15]+=j[76]- -1083,j[18]+=j[50]- -957,j[26]+=j[68]-729,j[29]+=j[68]-1062,j[46]+=j[24]-636,j[54]+=j[59]-2893,j[66]+=j[7]- -1287,j[67]+=j[88]- -787,j[71]+=j[8]- -1482,j[83]+=j[55]-1174;break;case 769:case j[54]- -1480:case 440:j[5]+=j[67]-2462,j[15]+=j[48]-2018,j[18]+=j[71]-1263,j[26]+=j[51]- -277,j[29]+=j[32]-2113,j[46]+=j[48]- -1528,j[54]+=j[3]- -541,j[66]+=j[68]- -890,j[67]+=j[45]- -313,j[71]+=j[1]-336,j[83]+=j[53]-162;break;case j[64]-1324:case 568:case-651:[k["f"]["a"],k["f"]["b"]={["a"]:{}},k["f"]["c"]]=s;while(n(k["f"]["a"])!==j[67]+1189){switch(n(k["f"]["a"])){case j[3]+-434:if(!k["c"]["k"]){k["f"]["a"][1]+=k["f"]["a"][j[77]+862]-(j[2]+5953),k["f"]["a"][2]+=k["f"]["a"][56]- -(j[20]+1030),k["f"]["a"][j[42]+886]+=k["f"]["a"][j[87]+411]- -(j[11]+1218),k["f"]["a"][j[74]+-859]+=k["f"]["a"][84]-(j[53]+1200),k["f"]["a"][j[19]+93]+=k["f"]["a"][j[28]+-48]- -(j[30]+3543),k["f"]["a"][j[67]+915]+=k["f"]["a"][j[56]+951]- -(j[17]+962),k["f"]["a"][j[28]+-87]+=k["f"]["a"][j[16]+658]-153,k["f"]["a"][j[84]+-570]+=k["f"]["a"][j[51]+-938]- -(j[9]+462),k["f"]["a"][j[54]+-354]+=k["f"]["a"][j[60]+-976]-(j[37]+903),k["f"]["a"][76]+=k["f"]["a"][66]- -(j[63]+1486);break}else{k["f"]["a"][j[34]+-925]+=k["f"]["a"][j[37]+927]-418,k["f"]["a"][2]+=k["f"]["a"][j[41]+826]-(j[31]+599),k["f"]["a"][7]+=k["f"]["a"][71]- -(j[49]+1730),k["f"]["a"][j[64]+-899]+=k["f"]["a"][j[80]+-793]- -(j[75]+-95),k["f"]["a"][j[18]+-466]+=k["f"]["a"][j[78]+171]- -3658,k["f"]["a"][37]+=k["f"]["a"][45]-(j[52]+4282),k["f"]["a"][j[44]+-162]+=k["f"]["a"][38]-182,k["f"]["a"][57]+=k["f"]["a"][j[79]+691]- -517,k["f"]["a"][60]+=k["f"]["a"][18]-31,k["f"]["a"][76]+=k["f"]["a"][j[82]+10]- -(j[54]+564);break}case 616:case k["f"]["a"][j[46]+999]-319:case-27:if(k["f"]["a"][j[86]+254]>-(j[46]+1437)){k["f"]["a"][1]+=k["f"]["a"][78]- -(j[64]+-610),k["f"]["a"][2]+=k["f"]["a"][8]- -1725,k["f"]["a"][j[76]+-84]+=k["f"]["a"][44]- -(j[37]+1186),k["f"]["a"][22]+=k["f"]["a"][j[39]+-812]-(j[24]+697),k["f"]["a"][33]+=k["f"]["a"][j[40]+879]-(j[9]+588),k["f"]["a"][j[1]+524]+=k["f"]["a"][j[14]+-55]- -(j[85]+739),k["f"]["a"][j[62]+-856]+=k["f"]["a"][44]- -(j[0]+-547),k["f"]["a"][57]+=k["f"]["a"][j[16]+673]- -1699,k["f"]["a"][60]+=k["f"]["a"][33]- -2039,k["f"]["a"][j[39]+-811]+=k["f"]["a"][17]- -170;break}k["f"]["a"][1]+=k["f"]["a"][63]- -(j[8]+1720),k["f"]["a"][j[30]+-843]+=k["f"]["a"][j[79]+751]-(j[0]+-103),k["f"]["a"][7]+=k["f"]["a"][j[14]+-32]- -1889,k["f"]["a"][22]+=k["f"]["a"][j[25]+531]-(j[3]+287),k["f"]["a"][33]+=k["f"]["a"][j[44]+-181]-1443,k["f"]["a"][j[19]+97]+=k["f"]["a"][j[1]+565]- -598,k["f"]["a"][40]+=k["f"]["a"][63]-(j[25]+2109),k["f"]["a"][j[12]+-345]+=k["f"]["a"][66]- -(j[86]+1289),k["f"]["a"][60]+=k["f"]["a"][j[6]+24]- -1537,k["f"]["a"][76]+=k["f"]["a"][16]- -(j[83]+1004);break;case k["f"]["a"][j[26]+934]-(j[43]+-603):case 468:case-758:k["f"]["a"][j[83]+985]+=k["f"]["a"][j[83]+1032]-(j[81]+6075),k["f"]["a"][j[69]+-582]+=k["f"]["a"][8]- -3299,k["f"]["a"][j[11]+838]+=k["f"]["a"][78]- -(j[39]+281),k["f"]["a"][22]+=k["f"]["a"][26]-1022,k["f"]["a"][33]+=k["f"]["a"][55]- -819,k["f"]["a"][j[36]+-840]+=k["f"]["a"][30]- -622,k["f"]["a"][40]+=k["f"]["a"][33]-615,k["f"]["a"][j[42]+936]+=k["f"]["a"][j[22]+-589]- -(j[61]+1872),k["f"]["a"][j[26]+984]+=k["f"]["a"][70]-317,k["f"]["a"][j[41]+855]+=k["f"]["a"][j[62]+-874]-85;break;case j[87]+905:k["f"]["a"][1]+=k["f"]["a"][80]-(j[80]+285),k["f"]["a"][2]+=k["f"]["a"][j[57]+745]-(j[32]+-895),k["f"]["a"][j[61]+617]+=k["f"]["a"][j[83]+1045]- -246,k["f"]["a"][22]+=k["f"]["a"][j[29]+981]- -(j[11]+1453),k["f"]["a"][j[35]+212]+=k["f"]["a"][j[5]+137]- -(j[28]+2963),k["f"]["a"][37]+=k["f"]["a"][12]-(j[6]+4854),k["f"]["a"][40]+=k["f"]["a"][76]- -548,k["f"]["a"][57]+=k["f"]["a"][j[31]+412]- -(j[17]+1271),k["f"]["a"][60]+=k["f"]["a"][j[78]+138]-(j[70]+813),k["f"]["a"][76]+=k["f"]["a"][j[6]+-4]- -(j[16]+1478);break;case k["f"]["a"][64]-(j[12]+1433):k["f"]["a"][1]+=k["f"]["a"][71]- -512,k["f"]["a"][j[84]+-625]+=k["f"]["a"][28]-(j[33]+-665),k["f"]["a"][j[84]+-620]+=k["f"]["a"][3]-757,k["f"]["a"][22]+=k["f"]["a"][55]- -(j[47]+444),k["f"]["a"][j[58]+-330]+=k["f"]["a"][28]-4107,k["f"]["a"][j[26]+961]+=k["f"]["a"][59]- -(j[62]+4214),k["f"]["a"][40]+=k["f"]["a"][j[66]+-86]-709,k["f"]["a"][j[72]+29]+=k["f"]["a"][16]-(j[3]+-111),k["f"]["a"][60]+=k["f"]["a"][j[24]+-789]-(j[68]+1651),k["f"]["a"][j[77]+919]+=k["f"]["a"][35]- -(j[4]+1490);break;case k["f"]["a"][17]- -(j[48]+-227):for(k["f"]["b"]["a"]["c"]=j[60]+-982;k["f"]["b"]["a"]["c"]>>k["f"]["a"][76]+947}return k["e"]["c"]=j[9]==972,k["f"]["b"]["a"]["b"];k["f"]["a"][1]+=k["f"]["a"][57]-39,k["f"]["a"][2]+=k["f"]["a"][63]- -2658,k["f"]["a"][j[39]+-880]+=k["f"]["a"][j[57]+759]- -83,k["f"]["a"][22]+=k["f"]["a"][j[52]+177]- -(j[71]+-80),k["f"]["a"][j[39]+-854]+=k["f"]["a"][41]- -431,k["f"]["a"][37]+=k["f"]["a"][j[40]+876]- -369,k["f"]["a"][40]+=k["f"]["a"][j[12]+-379]- -(j[45]+4231),k["f"]["a"][j[74]+-824]+=k["f"]["a"][j[30]+-832]-(j[81]+862),k["f"]["a"][j[77]+903]+=k["f"]["a"][j[13]+372]-141,k["f"]["a"][j[48]+-205]+=k["f"]["a"][50]-(j[41]+789);break;case-26:if(k["f"]["a"][j[68]+308]<-(k["f"]["a"][42]+1290)){k["f"]["a"][1]+=k["f"]["a"][11]- -(j[68]+796),k["f"]["a"][j[44]+-200]+=k["f"]["a"][79]-1456,k["f"]["a"][j[7]+616]+=k["f"]["a"][60]- -(j[67]+5028),k["f"]["a"][22]+=k["f"]["a"][j[26]+1007]-655,k["f"]["a"][33]+=k["f"]["a"][58]-766,k["f"]["a"][37]+=k["f"]["a"][j[40]+848]-101,k["f"]["a"][j[13]+379]+=k["f"]["a"][31]-731,k["f"]["a"][57]+=k["f"]["a"][23]- -274,k["f"]["a"][j[8]+591]+=k["f"]["a"][79]-193,k["f"]["a"][j[44]+-126]+=k["f"]["a"][81]- -(j[7]+2229);break}case j[54]+451:case 180:return k["e"]["c"]=j[67]!=957,k["e"]["b"];if(k["f"]["a"][57]==-(k["f"]["a"][26]+357)){;break}k["f"]["a"][j[24]+-798]+=k["f"]["a"][77]- -(j[6]+5354),k["f"]["a"][2]+=k["f"]["a"][26]-(j[26]+1917),k["f"]["a"][j[62]+-889]+=k["f"]["a"][j[42]+882]-(j[32]+-359),k["f"]["a"][22]+=k["f"]["a"][j[14]+-101]- -1478,k["f"]["a"][33]+=k["f"]["a"][j[76]+-26]-(j[5]+191),k["f"]["a"][j[0]+-619]+=k["f"]["a"][j[4]+721]-4317,k["f"]["a"][j[82]+-6]+=k["f"]["a"][j[57]+757]-35,k["f"]["a"][57]+=k["f"]["a"][j[41]+799]-(j[45]+1679),k["f"]["a"][j[58]+-303]+=k["f"]["a"][j[43]+-821]- -584,k["f"]["a"][76]+=k["f"]["a"][j[26]+969]- -(j[70]+235);break;case k["f"]["a"][57]- -(j[17]+1281):k["f"]["a"][1]+=k["f"]["a"][j[77]+892]-151,k["f"]["a"][2]+=k["f"]["a"][j[6]+-1]- -853,k["f"]["a"][7]+=k["f"]["a"][61]- -(j[13]+1036),k["f"]["a"][j[36]+-855]+=k["f"]["a"][j[29]+916]- -4945,k["f"]["a"][33]+=k["f"]["a"][j[60]+-914]- -673,k["f"]["a"][37]+=k["f"]["a"][30]-4382,k["f"]["a"][40]+=k["f"]["a"][8]-(j[64]+-538),k["f"]["a"][j[0]+-599]+=k["f"]["a"][59]-706,k["f"]["a"][j[3]+-529]+=k["f"]["a"][38]-(j[71]+74),k["f"]["a"][j[33]+-668]+=k["f"]["a"][25]-(j[78]+227);break;case-74:case k["f"]["a"][56]- -(j[24]+-21):case j[50]+1006:k["f"]["b"]["a"]["a"]=(1,k["c"]["a"])(k["e"]["b"]);k["f"]["b"]["a"]["b"]=new Uint32Array(k["f"]["b"]["a"]["a"][m(k["f"]["a"][k["f"]["a"][4]+711]+529784,2,6)]/(k["f"]["a"][62]+-892));k["f"]["a"][j[69]+-583]+=k["f"]["a"][j[49]+451]- -1074,k["f"]["a"][j[21]+-431]+=k["f"]["a"][j[57]+759]-(j[9]+-428),k["f"]["a"][j[75]+-230]+=k["f"]["a"][j[44]+-126]- -260,k["f"]["a"][22]+=k["f"]["a"][j[55]+-213]-(j[50]+1824),k["f"]["a"][33]+=k["f"]["a"][55]-(j[16]+1460),k["f"]["a"][37]+=k["f"]["a"][12]- -(j[49]+4059),k["f"]["a"][40]+=k["f"]["a"][j[27]+721]-2624,k["f"]["a"][57]+=k["f"]["a"][5]-(j[75]+60),k["f"]["a"][60]+=k["f"]["a"][j[17]+859]- -724,k["f"]["a"][76]+=k["f"]["a"][j[39]+-830]-1314;break;case 516:case-495:case 534:k["f"]["a"][1]+=k["f"]["a"][14]-1735,k["f"]["a"][2]+=k["f"]["a"][36]-(j[36]+-42),k["f"]["a"][j[14]+-98]+=k["f"]["a"][j[33]+-703]- -(j[1]+679),k["f"]["a"][22]+=k["f"]["a"][j[57]+736]-(j[3]+-274),k["f"]["a"][j[84]+-594]+=k["f"]["a"][j[14]+-90]-396,k["f"]["a"][j[27]+733]+=k["f"]["a"][j[40]+890]-(j[25]+5063),k["f"]["a"][40]+=k["f"]["a"][4]- -7891,k["f"]["a"][57]+=k["f"]["a"][j[46]+980]-(j[42]+1915),k["f"]["a"][j[66]+-60]+=k["f"]["a"][j[42]+931]-1146,k["f"]["a"][j[39]+-811]+=k["f"]["a"][j[87]+431]- -(j[56]+1153);break}}return undefined;case j[63]- -1545:case-220:case-248:k["c"]["o"]={["PC"]:j[36]+-877,[m(j[76]+218529,j[9]+-267,j[18]+427)]:j[57]+737,[m(j[62]+185056,713,7)]:2,[m(j[j[38]+214]+674130,725,4)]:j[34]+-923,[m(j[69]+736661,734,j[38]+206)]:4,[m(j[j[60]+-920]+425519,j[24]+-55,j[75]+-229)]:j[14]+-100,[m(j[52]+673396,j[37]+1656,10)]:j[75]+-231,[m(j[8]+587191,j[54]+-57,8)]:j[64]+-914};k["c"]["p"]=j[32]+-908;k["c"]["f"]=1;j[5]+=j[15]- -235,j[15]+=j[84]-750,j[18]+=j[37]- -1430,j[26]+=j[10]- -1274,j[29]+=j[56]-948,j[46]+=j[4]-1002,j[54]+=j[5]-924,j[66]+=j[40]- -1121,j[67]+=j[72]-1062,j[71]+=j[38]- -198,j[83]+=j[71]- -1227;break;case j[64]-978:case 764:[k["d"]["a"]]=s;return typeof Buffer!==m(j[28]+558206,786,9)?Buffer[m(j[2]+482955,797,4)](k["d"]["a"],m(j[50]+593126,807,6)):Uint8Array[m(j[9]+557591,814,4)](atob(k["d"]["a"]),function(j){return j["charCodeAt"](0)});return undefined;case-61:k["c"]["e"][m(j[40]+629926,893,9)][m(j[83]+759465,909,8)]=function(){return this["bytecode"][this["_regs"][this["_f"]+k["c"]["o"]["PC"]]++]};k["c"]["e"][m(j[65]+814649,923,9)][m(j[55]+388749,933,10)]=function(j,l,s,m){var n=j["fn"];var o=this["_regs"];var F=this["_regsTop"];var G=k["c"]["p"]+n["regCount"];var H=F+G;while(o["length"]{return performance["now"]()};var n=m();this["_pushFrame"](j,s,l,0);while(true){var p=this["_f"];var N=this["_regs"];var O=this["bytecode"];var P=N[p+k["c"]["o"]["PC"]];if(P>=O["length"]){break}N[p+k["c"]["o"]["PC"]]=P+1;var Q=this["bytecode"][P];if(typeof k["c"]["o"]["NOISE_PC"]==="number"){N[p+k["c"]["o"]["NOISE_PC"]]=(N[p+k["c"]["o"]["NOISE_PC"]]+1)%O["length"]}if(typeof k["c"]["o"]["NOISE_COUNTER"]==="number"){N[p+k["c"]["o"]["NOISE_COUNTER"]]++}if(typeof k["c"]["o"]["NOISE_MIRROR"]==="number"){N[p+k["c"]["o"]["NOISE_MIRROR"]]=Q}if(typeof k["c"]["o"]["NOISE_ACC"]==="number"){N[p+k["c"]["o"]["NOISE_ACC"]]=N[p+k["c"]["o"]["NOISE_ACC"]]^P|0}if(k["c"]["l"]){var R=m();var S=R-n>k["c"]["l"];n=R;if(S){for(var T=0;T>N[V+this["_operand"]()];break}case k["c"]["m"]["USHR"]:{var W=this["_operand"]();var ac=N[V+this["_operand"]()];N[V+W]=ac>>>N[V+this["_operand"]()];break}case k["c"]["m"]["LT"]:{var W=this["_operand"]();var ac=N[V+this["_operand"]()];N[V+W]=acN[V+this["_operand"]()];break}case k["c"]["m"]["LTE"]:{var W=this["_operand"]();var ac=N[V+this["_operand"]()];N[V+W]=ac<=N[V+this["_operand"]()];break}case k["c"]["m"]["GTE"]:{var W=this["_operand"]();var ac=N[V+this["_operand"]()];N[V+W]=ac>=N[V+this["_operand"]()];break}case k["c"]["m"]["EQ"]:{var W=this["_operand"]();var ac=N[V+this["_operand"]()];N[V+W]=ac===N[V+this["_operand"]()];break}case k["c"]["m"]["NEQ"]:{var W=this["_operand"]();var ac=N[V+this["_operand"]()];N[V+W]=ac!==N[V+this["_operand"]()];break}case k["c"]["m"]["LOOSE_EQ"]:{var W=this["_operand"]();var ac=N[V+this["_operand"]()];N[V+W]=ac==N[V+this["_operand"]()];break}case k["c"]["m"]["LOOSE_NEQ"]:{var W=this["_operand"]();var ac=N[V+this["_operand"]()];N[V+W]=ac!=N[V+this["_operand"]()];break}case k["c"]["m"]["IN"]:{var W=this["_operand"]();var ac=N[V+this["_operand"]()];N[V+W]=ac in N[V+this["_operand"]()];break}case k["c"]["m"]["INSTANCEOF"]:{var W=this["_operand"]();var Z=N[V+this["_operand"]()];N[V+W]=Z instanceof N[V+this["_operand"]()];break}case k["c"]["m"]["UNARY_NEG"]:{var W=this["_operand"]();N[V+W]=-N[V+this["_operand"]()];break}case k["c"]["m"]["UNARY_POS"]:{var W=this["_operand"]();N[V+W]=+N[V+this["_operand"]()];break}case k["c"]["m"]["UNARY_NOT"]:{var W=this["_operand"]();N[V+W]=!N[V+this["_operand"]()];break}case k["c"]["m"]["UNARY_BITNOT"]:{var W=this["_operand"]();N[V+W]=~N[V+this["_operand"]()];break}case k["c"]["m"]["TYPEOF"]:{var W=this["_operand"]();N[V+W]=typeof N[V+this["_operand"]()];break}case k["c"]["m"]["VOID"]:{var W=this["_operand"]();this["_operand"]();N[V+W]=undefined;break}case k["c"]["m"]["TYPEOF_SAFE"]:{var W=this["_operand"]();var ad=this["_constant"]();var ab=Object["prototype"]["hasOwnProperty"]["call"](this["globals"],ad)?this["globals"][ad]:undefined;N[V+W]=typeof ab;break}case k["c"]["m"]["JUMP"]:N[p+k["c"]["o"]["PC"]]=this["_operand"]();break;case k["c"]["m"]["JUMP_IF_FALSE"]:{var ae=this["_operand"]();var af=this["_operand"]();if(!N[V+ae]){N[p+k["c"]["o"]["PC"]]=af}break}case k["c"]["m"]["JUMP_IF_TRUE"]:{var ae=this["_operand"]();var af=this["_operand"]();if(N[V+ae]){N[p+k["c"]["o"]["PC"]]=af}break}case k["c"]["m"]["CALL"]:{var W=this["_operand"]();var ag=N[V+this["_operand"]()];var ah=this["_operand"]();var s;if(ah===k["c"]["n"]["CALL_SPREAD"]){s=N[V+this["_operand"]()]}else{s=new Array(ah);for(var T=0;T>1):0]=aj;break}case k["c"]["m"]["THROW"]:throw N[V+this["_operand"]()];case k["c"]["m"]["MAKE_CLOSURE"]:{var W=this["_operand"]();var an=this["_operand"]();var ao=this["_operand"]();var ap=this["_operand"]();var aq=this["_operand"]();var ar=this["_operand"]();var as=new Array(aq);for(var T=0;T=aO["_keys"]["length"]){N[p+k["c"]["o"]["PC"]]=aP}else{N[V+W]=aO["_keys"][aO["i"]++]}break}case k["c"]["m"]["TRY_SETUP"]:{var aQ=N[p+k["c"]["o"]["HANDLERS"]];if(!aQ){N[p+k["c"]["o"]["HANDLERS"]]=aQ=[]}aQ["push"]({["handlerPc"]:this["_operand"](),["exceptionReg"]:this["_operand"]()});break}case k["c"]["m"]["TRY_END"]:{N[p+k["c"]["o"]["HANDLERS"]]["pop"]();break}case k["c"]["m"]["FINALLY_SETUP"]:{var aQ=N[p+k["c"]["o"]["HANDLERS"]];if(!aQ){N[p+k["c"]["o"]["HANDLERS"]]=aQ=[]}aQ["push"]({["finallyPc"]:this["_operand"](),["contReg"]:this["_operand"](),["payloadReg"]:this["_operand"](),["throwPad"]:this["_operand"]()});break}case k["c"]["m"]["PATCH"]:{var aR=this["_operand"]();var aS=this["_operand"]();var aT=this["_operand"]();var aU=this["_operand"]()^aR|0;for(var aV=aS;aV>>13))>>>0}break}case k["c"]["m"]["JUMP_REG"]:{N[p+k["c"]["o"]["PC"]]=N[V+this["_operand"]()];break}case k["c"]["m"]["DEBUGGER"]:{debugger;break}default:throw new Error("Unknown opcode: "+Q+" at pc "+(N[p+k["c"]["o"]["PC"]]-1))}}catch(err){var aW=0;var aX=this["_f"];while(aX){var aY=N[aX+k["c"]["o"]["HANDLERS"]];if(aY&&aY["length"]>0){aW=aX;break}this["_closeUpvaluesFor"](aX);this["_regsTop"]=aX;aX=N[aX+k["c"]["o"]["CALLER"]];this["_f"]=aX}if(!aW){throw err}var aZ=N[aW+k["c"]["o"]["HANDLERS"]]["pop"]();var ba=N[aW+k["c"]["o"]["REG_BASE"]];if(aZ["exceptionReg"]!==undefined){N[ba+aZ["exceptionReg"]]=err;N[aW+k["c"]["o"]["PC"]]=aZ["handlerPc"]}else{N[ba+aZ["contReg"]]=aZ["throwPad"];N[ba+aZ["payloadReg"]]=err;N[aW+k["c"]["o"]["PC"]]=aZ["finallyPc"]}this["_regsTop"]=aW+N[aW+k["c"]["o"]["FRAME_SIZE"]];this["_f"]=aW}if(!this["_f"]){return this["_regs"][0]}}};if(j[j[46]+3121]>-617){j[5]+=j[29]-140,j[15]+=j[75]-901,j[18]+=j[69]-1461,j[26]+=j[25]-211,j[29]+=j[18]- -99,j[46]+=j[60]- -3456,j[54]+=j[7]- -2041,j[66]+=j[22]-1810,j[67]+=j[87]- -515,j[71]+=j[21]-708,j[83]+=j[78]-1468;break}j[5]+=j[72]-487,j[15]+=j[48]-6100,j[18]+=j[1]- -117,j[26]+=j[30]- -363,j[29]+=j[70]-278,j[46]+=j[19]- -3964,j[54]+=j[37]- -2168,j[66]+=j[49]-163,j[67]+=j[35]- -1077,j[71]+=j[32]-704,j[83]+=j[71]-1435;break}}}p([...o(0,5),228,...o(6,15),-426,...o(16,18),-833,...o(19,26),26,...o(27,29),683,...o(30,46),-618,...o(47,54),3201,...o(55,66),-248,-568,...o(68,71),-922,...o(72,83),278,...o(84,89)]); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/source.js b/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/source.js new file mode 100644 index 00000000..9f9d5e11 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/source.js @@ -0,0 +1,5 @@ +var value = 4 +if (value > 2) { + value = value + 3 +} +window.TEST_OUTPUT = value diff --git a/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/vm-only.js b/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/vm-only.js new file mode 100644 index 00000000..d5c0f116 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/layer-pair-arithmetic/vm-only.js @@ -0,0 +1,1035 @@ +// fn_0_0: +// [4, 1], LOAD_THIS reg[1] = this +// [0, 3, 0, 0], LOAD_CONST reg[3] = 4 1:12-1:13 +// [5, 2, 3], MOVE reg[2] = reg[3] 1:0-1:13 +// [0, 3, 1, 0], LOAD_CONST reg[3] = 2 2:12-2:13 +// [23, 4, 2, 3], GT reg[4] = reg[2] > reg[3] 2:4-2:13 +// [40, 4, 31], JUMP_IF_FALSE if (!reg[4]) goto if_else_1 2:0-4:1 +// [0, 3, 2, 0], LOAD_CONST reg[3] = 3 3:18-3:19 +// [11, 4, 2, 3], ADD reg[4] = reg[2] + reg[3] 3:10-3:19 +// [5, 2, 4], MOVE reg[2] = reg[4] 3:2-3:19 +// if_else_1: +// [2, 3, 3, 0], LOAD_GLOBAL reg[3] = window 5:0-5:6 +// [0, 4, 4, 0], LOAD_CONST reg[4] = "TEST_OUTPUT" 5:0-5:26 +// [9, 3, 4, 2], SET_PROP reg[3][reg[4]] = reg[2] 5:0-5:26 +// [0, 3, 5, 0], LOAD_CONST reg[3] = undefined +// [45, 3], RETURN reg[3] +var CONSTANTS = [/* 0 */4, /* 1 */2, /* 2 */3, /* 3 */"window", /* 4 */"TEST_OUTPUT", /* 5 */undefined]; +var BYTECODE = [4, 1, 0, 3, 0, 0, 5, 2, 3, 0, 3, 1, 0, 23, 4, 2, 3, 40, 4, 31, 0, 3, 2, 0, 11, 4, 2, 3, 5, 2, 4, 2, 3, 3, 0, 0, 4, 4, 0, 9, 3, 4, 2, 0, 3, 5, 0, 45, 3]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 5; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-172.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-172.encoded.js new file mode 100644 index 00000000..e72aa743 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-172.encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-173.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-173.encoded.js new file mode 100644 index 00000000..e72aa743 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-173.encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-174.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-174.encoded.js new file mode 100644 index 00000000..e72aa743 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/baseline-174.encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-172.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-172.encoded.js new file mode 100644 index 00000000..076106ef --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-172.encoded.js @@ -0,0 +1,952 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(absSlot, fake_1, regs) { + this.i = fake_1; + this.g = regs; // shared reference to VM._regs flat array + this.e = absSlot; // absolute index; stable as long as frame is alive + this.I = false; + this.P = undefined; +} +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(constants, fake_13, globals, bytecode) { + this.q = bytecode; + this.K = constants; + this.u = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.F = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.W = fake_13; + this.g = []; + this.s = 1; + this.n = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +// Closure & Frame +function Closure(fake_11, fake_9, fake_10, fake_12, fn) { + this.t = fn; + this.Z = []; + this.E = [fake_10, fake_11]; + this.prototype = {}; // <- default prototype object for `new` + this.aa = fake_9 || fake_12; +} +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +VM.prototype.R = function (fp, fake_18, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.g[fp + 7] + slot; + var uvs = this.F || (this.F = []); + this.U = fake_18; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, null, this.g)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.d = function (fake_16, closure, retDst, args, fake_17, thisVal) { + var fn = closure.t; + var regs = this.g; + var fp = this.s; + var size = 8 + fn.l; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 8; + regs[fp + 0] = fn.p; + regs[fp + 1] = this.n; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.T; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.p; + this.D = fake_17 && fake_16; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.s = end; + if (args) { + if (fn.r) { + var restSlot = fn.T - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.l; a++) regs[base + a] = args[a]; + } + if (fn.T < fn.l) regs[base + fn.T] = args; + } + this.n = fp; +}; +Upvalue.prototype.c = function (fake_4, v, fake_5, fake_3) { + this.m = fake_5; + if (this.I) this.P = v;else this.g[this.e] = v; + this.z = fake_3 || fake_4; +}; +VM.prototype.M = function (fake_20, idxIn, keyIn, fake_19) { + var idx = idxIn ?? this.N("k", "z"); + var key = keyIn ?? this.N(undefined, null); + var v = this.K[idx]; + this.Y = fake_20; + this.S = fake_19; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.B = function (fp, fake_21, fake_22) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.F; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this.g; + this.b = fake_21 || fake_22; + var hi = fp + regs[fp + 6]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.a("y", {}, null); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.a = function (fake_6, fake_7, fake_8) { + this.o = fake_6; + this.P = this.g[this.e]; + this.I = true; + this.Q = fake_7; + this.h = fake_8; +}; +Upvalue.prototype.w = function (fake_2) { + this.j = fake_2; + return this.I ? this.P : this.g[this.e]; +}; +VM.prototype.N = function (fake_15, fake_14) { + this.L = fake_15 || fake_14; + return this.q[this.g[this.n + 0]++]; +}; +VM.prototype.O = function (fake_23, fake_25, closure, args, thisVal, fake_24) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.G = fake_24; + this.v = [fake_23, fake_25]; + this.d(null, closure, 0, args, "y", thisVal); + while (true) { + var fp = this.n; + var regs = this.g; + var bc = this.q; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.q[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.q.length; i++) this.q[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.s; i2++) this.g[i2] = undefined; + op = 39; + regs[fp + 0] = this.q.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 0: + { + var dst = this.N(42, 53); + regs[base + dst] = this.M(); + break; + } + case 1: + { + var dst = this.N(null, undefined); + regs[base + dst] = this.N([], undefined); + break; + } + case 2: + { + var dst = this.N([], null); + var globalName = this.M(); + if (!(globalName in this.u)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.u[globalName]; + break; + } + case 3: + { + var dst = this.N({}, 55); + regs[base + dst] = regs[fp + 4].Z[this.N("z", {})].w(undefined); + break; + } + case 4: + { + var dst = this.N(undefined, "x"); + regs[base + dst] = regs[fp + 3]; + break; + } + case 5: + { + var dst = this.N(null, null); + regs[base + dst] = regs[base + this.N({}, 7)]; + break; + } + case 6: + { + // globals[globalName] = regs[src] + this.u[this.M()] = regs[base + this.N(undefined, [])]; + break; + } + case 7: + { + var uvIdx = this.N("z", 95); + regs[fp + 4].Z[uvIdx].c(undefined, regs[base + this.N(50, [])], null, "z"); + break; + } + case 8: + { + // dst = regs[obj][regs[key]] + var dst = this.N({}, []); + var obj = regs[base + this.N("q", undefined)]; + var key = regs[base + this.N(undefined, undefined)]; + regs[base + dst] = obj[key]; + break; + } + case 9: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.N({}, "k")]; + var key = regs[base + this.N(null, 58)]; + var val = regs[base + this.N(null, {})]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 10: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.N([], {}); + var obj = regs[base + this.N(undefined, "q")]; + var key = regs[base + this.N({}, 90)]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 11: + { + var dst = this.N(undefined, "k"); + var a = regs[base + this.N([], null)]; + regs[base + dst] = a + regs[base + this.N([], 53)]; + break; + } + case 12: + { + var dst = this.N([], "y"); + var a = regs[base + this.N({}, {})]; + regs[base + dst] = a - regs[base + this.N(undefined, null)]; + break; + } + case 13: + { + var dst = this.N(undefined, {}); + var a = regs[base + this.N(undefined, undefined)]; + regs[base + dst] = a * regs[base + this.N("z", "k")]; + break; + } + case 14: + { + var dst = this.N("x", 4); + var a = regs[base + this.N({}, "q")]; + regs[base + dst] = a / regs[base + this.N(26, [])]; + break; + } + case 15: + { + var dst = this.N([], null); + var a = regs[base + this.N([], undefined)]; + regs[base + dst] = a % regs[base + this.N({}, "q")]; + break; + } + case 60: + { + // Math.pow instead of `**` + var dst = this.N({}, 75); + var a = regs[base + this.N({}, 82)]; + regs[base + dst] = Math.pow(a, regs[base + this.N({}, null)]); + break; + } + case 16: + { + var dst = this.N("z", []); + var a = regs[base + this.N(null, {})]; + regs[base + dst] = a & regs[base + this.N(41, 80)]; + break; + } + case 17: + { + var dst = this.N({}, undefined); + var a = regs[base + this.N("y", [])]; + regs[base + dst] = a | regs[base + this.N([], {})]; + break; + } + case 18: + { + var dst = this.N(null, 16); + var a = regs[base + this.N("x", {})]; + regs[base + dst] = a ^ regs[base + this.N(null, null)]; + break; + } + case 19: + { + var dst = this.N(47, []); + var a = regs[base + this.N("z", [])]; + regs[base + dst] = a << regs[base + this.N([], [])]; + break; + } + case 20: + { + var dst = this.N({}, 37); + var a = regs[base + this.N(undefined, 53)]; + regs[base + dst] = a >> regs[base + this.N(10, {})]; + break; + } + case 21: + { + var dst = this.N({}, "x"); + var a = regs[base + this.N(undefined, [])]; + regs[base + dst] = a >>> regs[base + this.N(null, [])]; + break; + } + + // Comparison (dst, src1, src2) + case 22: + { + var dst = this.N([], undefined); + var a = regs[base + this.N(undefined, undefined)]; + regs[base + dst] = a < regs[base + this.N(null, 42)]; + break; + } + case 23: + { + var dst = this.N({}, null); + var a = regs[base + this.N("x", undefined)]; + regs[base + dst] = a > regs[base + this.N(null, 16)]; + break; + } + case 24: + { + var dst = this.N(null, 26); + var a = regs[base + this.N("x", 21)]; + regs[base + dst] = a <= regs[base + this.N("z", {})]; + break; + } + case 25: + { + var dst = this.N({}, 34); + var a = regs[base + this.N({}, null)]; + regs[base + dst] = a >= regs[base + this.N(undefined, {})]; + break; + } + case 26: + { + var dst = this.N(91, null); + var a = regs[base + this.N([], {})]; + regs[base + dst] = a === regs[base + this.N(null, {})]; + break; + } + case 27: + { + var dst = this.N(60, "x"); + var a = regs[base + this.N(10, {})]; + regs[base + dst] = a !== regs[base + this.N(3, null)]; + break; + } + case 28: + { + var dst = this.N(undefined, {}); + var a = regs[base + this.N({}, null)]; + regs[base + dst] = a == regs[base + this.N({}, "k")]; + break; + } + case 29: + { + var dst = this.N([], "z"); + var a = regs[base + this.N(52, "y")]; + regs[base + dst] = a != regs[base + this.N("z", 40)]; + break; + } + case 30: + { + var dst = this.N(11, null); + var a = regs[base + this.N({}, null)]; + regs[base + dst] = a in regs[base + this.N("q", null)]; + break; + } + case 31: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.N("x", []); + var obj = regs[base + this.N(null, undefined)]; + regs[base + dst] = obj instanceof regs[base + this.N(1, null)]; + break; + } + + // Unary (dst, src) + case 32: + { + var dst = this.N("y", []); + regs[base + dst] = -regs[base + this.N(76, [])]; + break; + } + case 33: + { + var dst = this.N(undefined, undefined); + regs[base + dst] = +regs[base + this.N(null, {})]; + break; + } + case 34: + { + var dst = this.N([], {}); + regs[base + dst] = !regs[base + this.N(91, 87)]; + break; + } + case 35: + { + var dst = this.N(null, "k"); + regs[base + dst] = ~regs[base + this.N(19, {})]; + break; + } + case 36: + { + var dst = this.N(44, 19); + regs[base + dst] = typeof regs[base + this.N([], {})]; + break; + } + case 37: + { + var dst = this.N("q", "x"); + this.N("k", undefined); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 38: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.N("x", null); + var name = this.M(); + var val = Object.prototype.hasOwnProperty.call(this.u, name) ? this.u[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39: + regs[fp + 0] = this.N({}, 89); + break; + case 40: + { + var src = this.N(undefined, null); + var target = this.N({}, []); + if (!regs[base + src]) regs[fp + 0] = target; + break; + } + case 41: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.N({}, null); + var target = this.N({}, null); + if (regs[base + src]) regs[fp + 0] = target; + break; + } + + // Calls + case 42: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.N("k", undefined); + var callee = regs[base + this.N({}, "z")]; + var argc = this.N([], 30); + var args; + if (argc === 65535) { + args = regs[base + this.N([], [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.N(null, [])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.d("q", closure, dst << 1, args, "q", this.u); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 43: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.N("k", []); + var receiver = regs[base + this.N(null, "y")]; + var callee = regs[base + this.N([], "k")]; + var argc = this.N([], null); + var args; + if (argc === 65535) { + args = regs[base + this.N([], null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.N(31, {})]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.d("z", closure, dst << 1, args, "z", receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 44: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.N([], "k"); + var callee = regs[base + this.N({}, 51)]; + var argc = this.N(63, []); + var args; + if (argc === 65535) { + args = regs[base + this.N(undefined, 78)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.N(69, null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.d(undefined, closure, dst << 1 | 1, args, [], Object.create(closure.prototype || null)); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 45: + { + var retVal = regs[base + this.N(null, "q")]; + this.B(fp, "k", []); // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.s = fp; + this.n = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 46: + throw regs[base + this.N([], null)]; + + // Closures + case 47: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.N("y", {}); + var startPc = this.N({}, null); + var paramCount = this.N(55, "x"); + var regCount = this.N(12, 29); + var uvCount = this.N([], "q"); + var hasRest = this.N(38, undefined); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.N(null, undefined); + var uvIndex = this.N({}, 2); + uvDescs[i] = { + J: isLocalRaw, + y: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + T: paramCount, + l: regCount, + p: startPc, + r: hasRest + }; + var closure = new Closure(51, null, [], 59, fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.J) { + closure.Z.push(this.R(fp, {}, uvd.y)); + } else { + closure.Z.push(regs[fp + 4].Z[uvd.y]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.K, undefined, self.u, self.q).O([], {}, c, Array.prototype.slice.call(arguments), this == null ? self.u : this, 45); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 48: + { + // dst, count, [elemReg...] + var dst = this.N([], undefined); + var count = this.N(undefined, "y"); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.N({}, [])]; + regs[base + dst] = elems; + break; + } + case 49: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.N({}, "z"); + var pairCount = this.N({}, null); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.N(null, 56)]; + var val = regs[base + this.N("k", undefined)]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50: + { + // obj, key, fn + var obj = regs[base + this.N([], null)]; + var key = regs[base + this.N([], 72)]; + var getterFn = regs[base + this.N({}, [])]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 51: + { + // obj, key, fn + var obj = regs[base + this.N(82, undefined)]; + var key = regs[base + this.N([], null)]; + var setterFn = regs[base + this.N("z", 88)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.N(22, null); + var obj = regs[base + this.N(undefined, undefined)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + x: keys, + X: 0 + }; + break; + } + case 53: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.N(4, undefined); + var iter = regs[base + this.N(58, "y")]; + var exitTarget = this.N("y", []); + if (iter.X >= iter.x.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.x[iter.X++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 54: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + H: this.N(2, []), + V: this.N("k", []) + }); + break; + } + case 55: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 59: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + k: this.N(undefined, 89), + A: this.N([], {}), + f: this.N("x", 31), + C: this.N("x", {}) + }); + break; + } + + // Self-modifying bytecode + case 56: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.N(null, undefined); + var sliceStart = this.N(null, undefined); + var sliceEnd = this.N(38, []); + var pk = this.N({}, "k") ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.q[destPc + (pi - sliceStart)] = (this.q[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 58: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.N(59, null)]; + break; + } + case 57: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 0] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.n; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.B(searchFrame, [], {}); + this.s = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.n = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.V !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.V] = err; + regs[handledFrame + 0] = h.H; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.A] = h.C; + regs[hBase + h.f] = err; + regs[handledFrame + 0] = h.k; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.s = handledFrame + regs[handledFrame + 6]; + this.n = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.n) return this.g[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(CONSTANTS, "x", globals, decodeBytecode(BYTECODE)); +vm.O("k", {}, new Closure(35, [], undefined, undefined, { + T: 0, + l: 12, + p: 0 +}), null // no arguments object / parameter setup for the root frame +, undefined, {}); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-173.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-173.encoded.js new file mode 100644 index 00000000..b81b1cda --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-173.encoded.js @@ -0,0 +1,966 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(fake_17, fake_15, globals, fake_16, bytecode, constants) { + this.z = bytecode; + this.r = constants; + this.K = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.e = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.G = fake_16; + this.A = []; + this.w = fake_15; + this.c = fake_17; + this.n = 1; + this.q = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) + +// Closure & Frame +function Closure(fake_14, fn) { + this.ad = fn; + this.C = []; + this.m = fake_14; + this.prototype = {}; // <- default prototype object for `new` +} +function Upvalue(absSlot, regs, fake_1) { + this.H = fake_1; + this.A = regs; // shared reference to VM._regs flat array + this.p = absSlot; // absolute index; stable as long as frame is alive + this.a = false; + this.ab = undefined; +} +Upvalue.prototype.y = function (fake_6, fake_7, fake_9, v, fake_8) { + if (this.a) this.ab = v;else this.A[this.p] = v; + this.W = [fake_6, fake_9]; + this.D = fake_7 && fake_8; +}; +VM.prototype.I = function (fake_18, fake_20, fake_19, fake_21) { + this.J = fake_21; + this.g = fake_20; + this.f = fake_18; + return this.z[this.A[this.q + 0]++]; + this.ae = fake_19; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). + +VM.prototype.R = function (fake_33, fake_34, fake_32, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.e; + this.X = fake_33; + this.u = fake_34; + if (!uvs) return; // nothing in this program ever captured a local + this.ag = fake_32; + var regs = this.A; + var hi = fp + regs[fp + 6]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.i("q", "z", {}, "q"); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.E = function (fake_3, fake_4, fake_2, fake_5) { + this.o = fake_3 || fake_5; + this.al = fake_2; + return this.a ? this.ab : this.A[this.p]; + this.O = fake_4; +}; +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.l = function (keyIn, idxIn, fake_30, fake_31) { + this.am = fake_30; + var idx = idxIn ?? this.I({}, {}, [], "z"); + var key = keyIn ?? this.I(undefined, [], null, []); + var v = this.r[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + this.S = fake_31; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.h = function (thisVal, fake_22, fake_23, fake_24, closure, fake_25, args, retDst) { + var fn = closure.ad; + var regs = this.A; + var fp = this.n; + var size = 8 + fn.U; + var end = fp + size; + this.x = fake_24; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + this.Q = [fake_25, fake_22]; + this.V = fake_23; + var base = fp + 8; + regs[fp + 0] = fn.j; + regs[fp + 1] = this.q; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.k; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.j; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.n = end; + if (args) { + if (fn.ak) { + var restSlot = fn.k - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.U; a++) regs[base + a] = args[a]; + } + if (fn.k < fn.U) regs[base + fn.k] = args; + } + this.q = fp; +}; +VM.prototype.t = function (fake_27, fake_26, fake_29, slot, fake_28, fp) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.A[fp + 7] + slot; + this.ah = fake_27 || fake_26; + this.F = fake_28 && fake_29; + var uvs = this.e || (this.e = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, this.A, [])); +}; +Upvalue.prototype.i = function (fake_13, fake_12, fake_11, fake_10) { + this.ab = this.A[this.p]; + this.B = [fake_13, fake_12]; + this.a = true; + this.Y = fake_11; + this.L = fake_10; +}; +VM.prototype.P = function (thisVal, fake_37, fake_35, fake_38, closure, fake_36, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.h(thisVal, [], null, undefined, closure, "x", args, 0); + this.aa = fake_37; + this.d = fake_38; + while (true) { + var fp = this.q; + var regs = this.A; + var bc = this.z; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.z[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.z.length; i++) this.z[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.n; i2++) this.A[i2] = undefined; + op = 39; + regs[fp + 0] = this.z.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 0: + { + var dst = this.I({}, [], "x", 66); + regs[base + dst] = this.l(); + break; + } + case 1: + { + var dst = this.I(4, {}, "y", "k"); + regs[base + dst] = this.I([], null, {}, 64); + break; + } + case 2: + { + var dst = this.I("x", 5, undefined, 91); + var globalName = this.l(); + if (!(globalName in this.K)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.K[globalName]; + break; + } + case 3: + { + var dst = this.I("y", null, undefined, {}); + regs[base + dst] = regs[fp + 4].C[this.I("y", [], undefined, undefined)].E(78, [], undefined, []); + break; + } + case 4: + { + var dst = this.I([], {}, "k", "y"); + regs[base + dst] = regs[fp + 3]; + break; + } + case 5: + { + var dst = this.I([], undefined, [], "k"); + regs[base + dst] = regs[base + this.I(43, null, 67, [])]; + break; + } + case 6: + { + // globals[globalName] = regs[src] + this.K[this.l()] = regs[base + this.I([], undefined, 65, null)]; + break; + } + case 7: + { + var uvIdx = this.I({}, null, undefined, []); + regs[fp + 4].C[uvIdx].y([], {}, [], regs[base + this.I("y", undefined, 89, 54)], {}); + break; + } + case 8: + { + // dst = regs[obj][regs[key]] + var dst = this.I(null, {}, [], "x"); + var obj = regs[base + this.I(29, [], 40, null)]; + var key = regs[base + this.I(8, "y", null, {})]; + regs[base + dst] = obj[key]; + break; + } + case 9: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.I(undefined, undefined, {}, 35)]; + var key = regs[base + this.I({}, 55, null, [])]; + var val = regs[base + this.I([], [], 59, 84)]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 10: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.I({}, {}, "k", "k"); + var obj = regs[base + this.I(null, "z", [], undefined)]; + var key = regs[base + this.I([], null, null, undefined)]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 11: + { + var dst = this.I(null, undefined, 13, 74); + var a = regs[base + this.I({}, null, "z", null)]; + regs[base + dst] = a + regs[base + this.I(null, null, null, 8)]; + break; + } + case 12: + { + var dst = this.I(4, null, {}, undefined); + var a = regs[base + this.I([], null, {}, [])]; + regs[base + dst] = a - regs[base + this.I("k", [], [], undefined)]; + break; + } + case 13: + { + var dst = this.I(null, undefined, [], undefined); + var a = regs[base + this.I(undefined, {}, 96, "q")]; + regs[base + dst] = a * regs[base + this.I(undefined, undefined, [], {})]; + break; + } + case 14: + { + var dst = this.I(undefined, {}, "q", []); + var a = regs[base + this.I(74, 52, {}, 63)]; + regs[base + dst] = a / regs[base + this.I(undefined, "z", [], [])]; + break; + } + case 15: + { + var dst = this.I([], undefined, [], "k"); + var a = regs[base + this.I(34, 11, 14, [])]; + regs[base + dst] = a % regs[base + this.I([], "y", "k", undefined)]; + break; + } + case 60: + { + // Math.pow instead of `**` + var dst = this.I("y", {}, [], undefined); + var a = regs[base + this.I({}, 80, {}, {})]; + regs[base + dst] = Math.pow(a, regs[base + this.I(undefined, 78, {}, {})]); + break; + } + case 16: + { + var dst = this.I(null, {}, 41, {}); + var a = regs[base + this.I(null, [], 45, 54)]; + regs[base + dst] = a & regs[base + this.I(null, 92, null, "x")]; + break; + } + case 17: + { + var dst = this.I(null, undefined, null, null); + var a = regs[base + this.I(undefined, [], "k", [])]; + regs[base + dst] = a | regs[base + this.I([], [], 50, null)]; + break; + } + case 18: + { + var dst = this.I(44, [], null, {}); + var a = regs[base + this.I("x", undefined, null, null)]; + regs[base + dst] = a ^ regs[base + this.I(0, null, "q", {})]; + break; + } + case 19: + { + var dst = this.I(null, "k", null, {}); + var a = regs[base + this.I("k", undefined, "x", {})]; + regs[base + dst] = a << regs[base + this.I(null, 79, null, [])]; + break; + } + case 20: + { + var dst = this.I({}, null, undefined, undefined); + var a = regs[base + this.I({}, "z", null, 82)]; + regs[base + dst] = a >> regs[base + this.I(null, 72, "q", null)]; + break; + } + case 21: + { + var dst = this.I(62, null, undefined, null); + var a = regs[base + this.I([], undefined, null, "q")]; + regs[base + dst] = a >>> regs[base + this.I(null, "z", null, null)]; + break; + } + + // Comparison (dst, src1, src2) + case 22: + { + var dst = this.I("k", {}, [], 22); + var a = regs[base + this.I(null, undefined, undefined, 48)]; + regs[base + dst] = a < regs[base + this.I({}, "x", null, "z")]; + break; + } + case 23: + { + var dst = this.I(28, "k", {}, {}); + var a = regs[base + this.I(88, [], undefined, [])]; + regs[base + dst] = a > regs[base + this.I("z", "y", [], undefined)]; + break; + } + case 24: + { + var dst = this.I([], undefined, [], "x"); + var a = regs[base + this.I(89, [], "k", [])]; + regs[base + dst] = a <= regs[base + this.I([], "x", null, null)]; + break; + } + case 25: + { + var dst = this.I({}, [], {}, []); + var a = regs[base + this.I(12, "q", null, {})]; + regs[base + dst] = a >= regs[base + this.I([], null, {}, null)]; + break; + } + case 26: + { + var dst = this.I(undefined, null, undefined, "y"); + var a = regs[base + this.I([], [], undefined, {})]; + regs[base + dst] = a === regs[base + this.I([], {}, {}, "z")]; + break; + } + case 27: + { + var dst = this.I({}, null, "x", undefined); + var a = regs[base + this.I([], undefined, null, "y")]; + regs[base + dst] = a !== regs[base + this.I(14, "z", [], [])]; + break; + } + case 28: + { + var dst = this.I("z", null, 66, []); + var a = regs[base + this.I({}, null, 88, 0)]; + regs[base + dst] = a == regs[base + this.I([], undefined, undefined, 67)]; + break; + } + case 29: + { + var dst = this.I([], "q", null, "z"); + var a = regs[base + this.I(null, null, undefined, "k")]; + regs[base + dst] = a != regs[base + this.I(null, {}, null, [])]; + break; + } + case 30: + { + var dst = this.I(undefined, [], {}, null); + var a = regs[base + this.I(null, 97, undefined, 17)]; + regs[base + dst] = a in regs[base + this.I({}, undefined, 11, undefined)]; + break; + } + case 31: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.I([], {}, "y", "k"); + var obj = regs[base + this.I([], "k", "k", [])]; + regs[base + dst] = obj instanceof regs[base + this.I("k", 69, null, 29)]; + break; + } + + // Unary (dst, src) + case 32: + { + var dst = this.I("z", undefined, undefined, undefined); + regs[base + dst] = -regs[base + this.I(39, {}, 96, null)]; + break; + } + case 33: + { + var dst = this.I([], [], 31, "x"); + regs[base + dst] = +regs[base + this.I({}, "y", "q", null)]; + break; + } + case 34: + { + var dst = this.I("y", "q", 59, null); + regs[base + dst] = !regs[base + this.I(68, undefined, 75, [])]; + break; + } + case 35: + { + var dst = this.I(null, undefined, [], "x"); + regs[base + dst] = ~regs[base + this.I(71, "z", [], 1)]; + break; + } + case 36: + { + var dst = this.I(null, {}, undefined, null); + regs[base + dst] = typeof regs[base + this.I({}, null, [], 44)]; + break; + } + case 37: + { + var dst = this.I("y", "q", {}, "y"); + this.I([], [], 42, "z"); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 38: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.I(undefined, [], null, "x"); + var name = this.l(); + var val = Object.prototype.hasOwnProperty.call(this.K, name) ? this.K[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39: + regs[fp + 0] = this.I(40, undefined, 39, 28); + break; + case 40: + { + var src = this.I(undefined, {}, [], undefined); + var target = this.I(undefined, {}, undefined, null); + if (!regs[base + src]) regs[fp + 0] = target; + break; + } + case 41: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.I([], undefined, {}, null); + var target = this.I(null, "k", 45, 6); + if (regs[base + src]) regs[fp + 0] = target; + break; + } + + // Calls + case 42: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.I(undefined, {}, 26, undefined); + var callee = regs[base + this.I(null, undefined, "z", [])]; + var argc = this.I({}, undefined, undefined, {}); + var args; + if (argc === 65535) { + args = regs[base + this.I([], 46, null, "q")]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.I("q", [], null, [])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.h(this.K, [], 1, [], closure, 78, args, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 43: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.I("k", 76, 31, undefined); + var receiver = regs[base + this.I("x", {}, undefined, {})]; + var callee = regs[base + this.I(39, undefined, 83, 76)]; + var argc = this.I(53, "y", undefined, "k"); + var args; + if (argc === 65535) { + args = regs[base + this.I(44, null, null, {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.I({}, [], {}, null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.h(receiver, "x", 81, undefined, closure, [], args, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 44: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.I([], 86, {}, "k"); + var callee = regs[base + this.I("y", [], "x", null)]; + var argc = this.I("z", [], [], null); + var args; + if (argc === 65535) { + args = regs[base + this.I("k", 32, [], 27)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.I(undefined, 95, null, 7)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.h(Object.create(closure.prototype || null), {}, undefined, "z", closure, [], args, dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 45: + { + var retVal = regs[base + this.I(undefined, [], [], null)]; + this.R({}, [], {}, fp); // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.n = fp; + this.q = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 46: + throw regs[base + this.I("q", null, [], "k")]; + + // Closures + case 47: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.I({}, "y", [], "q"); + var startPc = this.I([], undefined, [], []); + var paramCount = this.I(17, 85, null, {}); + var regCount = this.I([], null, 15, 36); + var uvCount = this.I([], [], undefined, undefined); + var hasRest = this.I(null, 93, null, undefined); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.I("z", {}, null, undefined); + var uvIndex = this.I({}, "k", {}, null); + uvDescs[i] = { + s: isLocalRaw, + N: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + k: paramCount, + U: regCount, + j: startPc, + ak: hasRest + }; + var closure = new Closure(73, fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.s) { + closure.C.push(this.t("y", 42, 61, uvd.N, undefined, fp)); + } else { + closure.C.push(regs[fp + 4].C[uvd.N]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM("q", null, self.K, undefined, self.z, self.r).P(this == null ? self.K : this, 6, null, {}, c, {}, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 48: + { + // dst, count, [elemReg...] + var dst = this.I(91, [], undefined, undefined); + var count = this.I({}, "k", [], undefined); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.I(3, "k", [], [])]; + regs[base + dst] = elems; + break; + } + case 49: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.I(undefined, null, {}, undefined); + var pairCount = this.I([], "k", [], []); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.I({}, "q", undefined, "y")]; + var val = regs[base + this.I(undefined, {}, "q", [])]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50: + { + // obj, key, fn + var obj = regs[base + this.I([], "k", [], null)]; + var key = regs[base + this.I(null, 59, [], 65)]; + var getterFn = regs[base + this.I([], "y", undefined, "y")]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 51: + { + // obj, key, fn + var obj = regs[base + this.I([], "k", undefined, undefined)]; + var key = regs[base + this.I(null, null, {}, 42)]; + var setterFn = regs[base + this.I({}, [], 59, undefined)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.I("x", 55, null, 50); + var obj = regs[base + this.I(69, undefined, 83, 55)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + T: keys, + ac: 0 + }; + break; + } + case 53: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.I([], {}, null, "z"); + var iter = regs[base + this.I({}, undefined, undefined, [])]; + var exitTarget = this.I(null, 32, undefined, {}); + if (iter.ac >= iter.T.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.T[iter.ac++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 54: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + af: this.I([], {}, "y", "y"), + b: this.I(undefined, 8, "k", null) + }); + break; + } + case 55: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 59: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + M: this.I(undefined, {}, "k", null), + Z: this.I(undefined, {}, 95, null), + v: this.I("z", 50, [], undefined), + aj: this.I(undefined, null, 81, []) + }); + break; + } + + // Self-modifying bytecode + case 56: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.I(undefined, "y", {}, "x"); + var sliceStart = this.I({}, [], null, 46); + var sliceEnd = this.I(undefined, {}, {}, "q"); + var pk = this.I({}, undefined, "x", []) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.z[destPc + (pi - sliceStart)] = (this.z[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 58: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.I(undefined, {}, 58, 43)]; + break; + } + case 57: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 0] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.q; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.R([], [], null, searchFrame); + this.n = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.q = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.b !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.b] = err; + regs[handledFrame + 0] = h.af; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.Z] = h.aj; + regs[hBase + h.v] = err; + regs[handledFrame + 0] = h.M; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.n = handledFrame + regs[handledFrame + 6]; + this.q = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.q) return this.A[0]; + } + this.ai = fake_36 || fake_35; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM({}, {}, globals, "z", decodeBytecode(BYTECODE), CONSTANTS); +vm.P(undefined, 38, 34, undefined, new Closure(null, { + k: 0, + U: 12, + j: 0 +}), "y", null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-174.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-174.encoded.js new file mode 100644 index 00000000..c9321783 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/classObfuscation-174.encoded.js @@ -0,0 +1,946 @@ +function VM(constants, bytecode, fake_13, fake_15, fake_14, fake_12, globals) { + this.G = bytecode; + this.w = constants; + this.Z = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.E = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.r = fake_14; + this.O = []; + this.z = fake_12 && fake_15; + this.M = fake_13; + this.L = 1; + this.U = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +// Closure & Frame +function Closure(fake_10, fake_11, fn) { + this.g = [fake_10, fake_11]; + this.l = fn; + this.D = []; + this.prototype = {}; // <- default prototype object for `new` +} +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(fake_1, fake_2, regs, absSlot, fake_3) { + this.O = regs; // shared reference to VM._regs flat array + this.u = fake_2 || fake_3; + this.j = fake_1; + this.q = absSlot; // absolute index; stable as long as frame is alive + this.b = false; + this.a = undefined; +} +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CLOSURE_MAP = new WeakMap(); +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +Upvalue.prototype.V = function (fake_5, fake_4, fake_6) { + return this.b ? this.a : this.O[this.q]; + this.C = fake_6; + this.Y = fake_4; + this.i = fake_5; +}; +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.c = function (closure, fake_20, retDst, args, fake_19, thisVal, fake_18) { + var fn = closure.l; + var regs = this.O; + var fp = this.L; + var size = 8 + fn.n; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 8; + regs[fp + 0] = fn.aa; + this.X = fake_20; + regs[fp + 1] = this.U; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.h; + this.W = fake_19; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.aa; + this.B = fake_18; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.L = end; + if (args) { + if (fn.R) { + var restSlot = fn.h - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.n; a++) regs[base + a] = args[a]; + } + if (fn.h < fn.n) regs[base + fn.h] = args; + } + this.U = fp; +}; +VM.prototype.ab = function (fake_25, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.E; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this.O; + var hi = fp + regs[fp + 6]; + this.v = fake_25; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.m(null, {}); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.J = function (fp, fake_22, fake_21, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.O[fp + 7] + slot; + var uvs = this.E || (this.E = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(undefined, null, this.O, absSlot, [])); + this.H = [fake_22, fake_21]; +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.k = function (idxIn, fake_24, fake_23, keyIn) { + var idx = idxIn ?? this.o(undefined, 53); + var key = keyIn ?? this.o(null, "y"); + var v = this.w[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + this.d = [fake_24, fake_23]; + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +Upvalue.prototype.F = function (fake_7, v) { + this.K = fake_7; + if (this.b) this.a = v;else this.O[this.q] = v; +}; +Upvalue.prototype.m = function (fake_9, fake_8) { + this.a = this.O[this.q]; + this.b = true; + this.A = [fake_8, fake_9]; +}; +VM.prototype.o = function (fake_17, fake_16) { + this.T = [fake_16, fake_17]; + return this.G[this.O[this.U + 0]++]; +}; +VM.prototype.t = function (thisVal, fake_26, args, fake_28, closure, fake_27) { + var now = () => { + return performance.now(); + }; + this.f = fake_26; + var lastTime = now(); + this.c(closure, undefined, 0, args, "x", thisVal, undefined); + while (true) { + var fp = this.U; + var regs = this.O; + var bc = this.G; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.G[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.G.length; i++) this.G[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.L; i2++) this.O[i2] = undefined; + op = 39; + regs[fp + 0] = this.G.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 0: + { + var dst = this.o([], {}); + regs[base + dst] = this.k(); + break; + } + case 1: + { + var dst = this.o([], 12); + regs[base + dst] = this.o("y", null); + break; + } + case 2: + { + var dst = this.o(undefined, undefined); + var globalName = this.k(); + if (!(globalName in this.Z)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.Z[globalName]; + break; + } + case 3: + { + var dst = this.o(undefined, undefined); + regs[base + dst] = regs[fp + 4].D[this.o([], 81)].V("q", "y", 9); + break; + } + case 4: + { + var dst = this.o([], undefined); + regs[base + dst] = regs[fp + 3]; + break; + } + case 5: + { + var dst = this.o("k", []); + regs[base + dst] = regs[base + this.o(82, [])]; + break; + } + case 6: + { + // globals[globalName] = regs[src] + this.Z[this.k()] = regs[base + this.o({}, "z")]; + break; + } + case 7: + { + var uvIdx = this.o(undefined, undefined); + regs[fp + 4].D[uvIdx].F(57, regs[base + this.o(44, undefined)]); + break; + } + case 8: + { + // dst = regs[obj][regs[key]] + var dst = this.o({}, null); + var obj = regs[base + this.o(undefined, undefined)]; + var key = regs[base + this.o([], 64)]; + regs[base + dst] = obj[key]; + break; + } + case 9: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.o({}, null)]; + var key = regs[base + this.o("z", undefined)]; + var val = regs[base + this.o(null, {})]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 10: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.o(85, undefined); + var obj = regs[base + this.o(undefined, [])]; + var key = regs[base + this.o("q", 64)]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 11: + { + var dst = this.o("z", []); + var a = regs[base + this.o(70, [])]; + regs[base + dst] = a + regs[base + this.o([], 95)]; + break; + } + case 12: + { + var dst = this.o(null, {}); + var a = regs[base + this.o(null, 88)]; + regs[base + dst] = a - regs[base + this.o([], {})]; + break; + } + case 13: + { + var dst = this.o("x", []); + var a = regs[base + this.o("k", {})]; + regs[base + dst] = a * regs[base + this.o(undefined, null)]; + break; + } + case 14: + { + var dst = this.o(null, undefined); + var a = regs[base + this.o("z", 3)]; + regs[base + dst] = a / regs[base + this.o("k", 77)]; + break; + } + case 15: + { + var dst = this.o([], undefined); + var a = regs[base + this.o(null, [])]; + regs[base + dst] = a % regs[base + this.o(null, "k")]; + break; + } + case 60: + { + // Math.pow instead of `**` + var dst = this.o(undefined, 37); + var a = regs[base + this.o("y", {})]; + regs[base + dst] = Math.pow(a, regs[base + this.o([], "x")]); + break; + } + case 16: + { + var dst = this.o([], "x"); + var a = regs[base + this.o({}, "z")]; + regs[base + dst] = a & regs[base + this.o("y", 15)]; + break; + } + case 17: + { + var dst = this.o(null, 51); + var a = regs[base + this.o([], [])]; + regs[base + dst] = a | regs[base + this.o(null, undefined)]; + break; + } + case 18: + { + var dst = this.o([], {}); + var a = regs[base + this.o({}, {})]; + regs[base + dst] = a ^ regs[base + this.o({}, [])]; + break; + } + case 19: + { + var dst = this.o(32, null); + var a = regs[base + this.o(56, [])]; + regs[base + dst] = a << regs[base + this.o(87, {})]; + break; + } + case 20: + { + var dst = this.o([], null); + var a = regs[base + this.o(74, [])]; + regs[base + dst] = a >> regs[base + this.o({}, "y")]; + break; + } + case 21: + { + var dst = this.o([], []); + var a = regs[base + this.o({}, null)]; + regs[base + dst] = a >>> regs[base + this.o(undefined, 50)]; + break; + } + + // Comparison (dst, src1, src2) + case 22: + { + var dst = this.o(null, []); + var a = regs[base + this.o(undefined, 3)]; + regs[base + dst] = a < regs[base + this.o(undefined, "k")]; + break; + } + case 23: + { + var dst = this.o(59, null); + var a = regs[base + this.o("y", 48)]; + regs[base + dst] = a > regs[base + this.o([], {})]; + break; + } + case 24: + { + var dst = this.o(null, 17); + var a = regs[base + this.o([], 98)]; + regs[base + dst] = a <= regs[base + this.o("z", "k")]; + break; + } + case 25: + { + var dst = this.o(undefined, 82); + var a = regs[base + this.o([], [])]; + regs[base + dst] = a >= regs[base + this.o(undefined, undefined)]; + break; + } + case 26: + { + var dst = this.o(null, {}); + var a = regs[base + this.o(undefined, "k")]; + regs[base + dst] = a === regs[base + this.o(74, [])]; + break; + } + case 27: + { + var dst = this.o(undefined, "k"); + var a = regs[base + this.o(null, {})]; + regs[base + dst] = a !== regs[base + this.o(3, null)]; + break; + } + case 28: + { + var dst = this.o({}, []); + var a = regs[base + this.o(29, undefined)]; + regs[base + dst] = a == regs[base + this.o({}, "x")]; + break; + } + case 29: + { + var dst = this.o({}, []); + var a = regs[base + this.o(null, "z")]; + regs[base + dst] = a != regs[base + this.o([], undefined)]; + break; + } + case 30: + { + var dst = this.o("k", "k"); + var a = regs[base + this.o([], 66)]; + regs[base + dst] = a in regs[base + this.o(null, null)]; + break; + } + case 31: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.o(undefined, null); + var obj = regs[base + this.o("q", null)]; + regs[base + dst] = obj instanceof regs[base + this.o(undefined, [])]; + break; + } + + // Unary (dst, src) + case 32: + { + var dst = this.o([], 19); + regs[base + dst] = -regs[base + this.o(undefined, 30)]; + break; + } + case 33: + { + var dst = this.o("x", {}); + regs[base + dst] = +regs[base + this.o(60, 97)]; + break; + } + case 34: + { + var dst = this.o([], null); + regs[base + dst] = !regs[base + this.o(null, {})]; + break; + } + case 35: + { + var dst = this.o(6, null); + regs[base + dst] = ~regs[base + this.o(null, undefined)]; + break; + } + case 36: + { + var dst = this.o("y", 22); + regs[base + dst] = typeof regs[base + this.o({}, 11)]; + break; + } + case 37: + { + var dst = this.o({}, null); + this.o("q", "x"); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 38: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.o(undefined, "q"); + var name = this.k(); + var val = Object.prototype.hasOwnProperty.call(this.Z, name) ? this.Z[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39: + regs[fp + 0] = this.o("z", "z"); + break; + case 40: + { + var src = this.o("y", undefined); + var target = this.o([], undefined); + if (!regs[base + src]) regs[fp + 0] = target; + break; + } + case 41: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.o("k", "q"); + var target = this.o(53, []); + if (regs[base + src]) regs[fp + 0] = target; + break; + } + + // Calls + case 42: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.o([], null); + var callee = regs[base + this.o({}, {})]; + var argc = this.o(undefined, []); + var args; + if (argc === 65535) { + args = regs[base + this.o(null, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.o(null, undefined)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.c(closure, "x", dst << 1, args, [], this.Z, null); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 43: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.o(null, []); + var receiver = regs[base + this.o({}, undefined)]; + var callee = regs[base + this.o(undefined, null)]; + var argc = this.o(null, null); + var args; + if (argc === 65535) { + args = regs[base + this.o({}, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.o(null, "z")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.c(closure, 57, dst << 1, args, [], receiver, {}); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 44: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.o([], "y"); + var callee = regs[base + this.o([], "y")]; + var argc = this.o(null, "z"); + var args; + if (argc === 65535) { + args = regs[base + this.o(20, [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.o([], null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.c(closure, 30, dst << 1 | 1, args, "q", Object.create(closure.prototype || null), []); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 45: + { + var retVal = regs[base + this.o({}, 13)]; + this.ab(null, fp); // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.L = fp; + this.U = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 46: + throw regs[base + this.o("y", null)]; + + // Closures + case 47: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.o("q", undefined); + var startPc = this.o(undefined, []); + var paramCount = this.o("z", null); + var regCount = this.o([], []); + var uvCount = this.o(undefined, {}); + var hasRest = this.o(89, null); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.o([], []); + var uvIndex = this.o({}, []); + uvDescs[i] = { + I: isLocalRaw, + Q: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + h: paramCount, + n: regCount, + aa: startPc, + R: hasRest + }; + var closure = new Closure(45, undefined, fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.I) { + closure.D.push(this.J(fp, 79, 73, uvd.Q)); + } else { + closure.D.push(regs[fp + 4].D[uvd.Q]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.w, self.G, 2, null, null, undefined, self.Z).t(this == null ? self.Z : this, 36, Array.prototype.slice.call(arguments), 66, c, 70); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 48: + { + // dst, count, [elemReg...] + var dst = this.o({}, 67); + var count = this.o(undefined, "x"); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.o(null, 63)]; + regs[base + dst] = elems; + break; + } + case 49: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.o(91, undefined); + var pairCount = this.o({}, null); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.o([], 79)]; + var val = regs[base + this.o("k", "x")]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50: + { + // obj, key, fn + var obj = regs[base + this.o(null, "q")]; + var key = regs[base + this.o(undefined, undefined)]; + var getterFn = regs[base + this.o([], null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 51: + { + // obj, key, fn + var obj = regs[base + this.o(null, null)]; + var key = regs[base + this.o(undefined, {})]; + var setterFn = regs[base + this.o(93, null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.o(null, undefined); + var obj = regs[base + this.o(null, null)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + x: keys, + N: 0 + }; + break; + } + case 53: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.o({}, undefined); + var iter = regs[base + this.o({}, undefined)]; + var exitTarget = this.o({}, 25); + if (iter.N >= iter.x.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.x[iter.N++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 54: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + e: this.o(undefined, undefined), + ac: this.o(null, 44) + }); + break; + } + case 55: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 59: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + P: this.o(undefined, "q"), + y: this.o(null, "y"), + s: this.o(undefined, null), + p: this.o(null, "z") + }); + break; + } + + // Self-modifying bytecode + case 56: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.o("q", {}); + var sliceStart = this.o({}, 5); + var sliceEnd = this.o(26, null); + var pk = this.o({}, []) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.G[destPc + (pi - sliceStart)] = (this.G[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 58: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.o(undefined, undefined)]; + break; + } + case 57: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 0] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.U; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.ab(undefined, searchFrame); + this.L = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.U = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.ac !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.ac] = err; + regs[handledFrame + 0] = h.e; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.y] = h.p; + regs[hBase + h.s] = err; + regs[handledFrame + 0] = h.P; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.L = handledFrame + regs[handledFrame + 6]; + this.U = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.U) return this.O[0]; + } + this.S = fake_28 || fake_27; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(CONSTANTS, decodeBytecode(BYTECODE), "y", [], {}, [], globals); +vm.t(undefined, undefined, null // no arguments object / parameter setup for the root frame +, undefined, new Closure({}, "k", { + h: 0, + n: 12, + aa: 0 +}), null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/exception-routes.expected.json b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/exception-routes.expected.json new file mode 100644 index 00000000..21dfdb95 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/exception-routes.expected.json @@ -0,0 +1,19 @@ +[ + 1, + 2, + 3, + 4, + [ + "try:0", + "catch:SyntaxError", + "finally:0", + "try:1", + "catch:TypeError", + "finally:1", + "try:2", + "catch:Error", + "finally:2", + "try:3", + "finally:3" + ] +] diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/exception-routes.source.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/exception-routes.source.js new file mode 100644 index 00000000..7b4d094e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/exception-routes.source.js @@ -0,0 +1 @@ +const trace = []; function make() { let count = 0; return function run(mode) { try { trace.push("try:" + mode); if (mode === 0) JSON.parse("{"); if (mode === 1) { const value = null; value.missing; } if (mode === 2) throw new Error("explicit"); return ++count; } catch (error) { trace.push("catch:" + error.name); return ++count; } finally { trace.push("finally:" + mode); } }; } const run = make(); window.TEST_OUTPUT = [run(0), run(1), run(2), run(3), trace]; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-172.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-172.encoded.js new file mode 100644 index 00000000..c1ec34ed --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-172.encoded.js @@ -0,0 +1,18 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new C([4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237, +2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],U),new B({m:0,v:12,G:0}),void 0, +null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-173.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-173.encoded.js new file mode 100644 index 00000000..c1ec34ed --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-173.encoded.js @@ -0,0 +1,18 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new C([4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237, +2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],U),new B({m:0,v:12,G:0}),void 0, +null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-174.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-174.encoded.js new file mode 100644 index 00000000..c1ec34ed --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minify-174.encoded.js @@ -0,0 +1,18 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new C([4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237, +2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],U),new B({m:0,v:12,G:0}),void 0, +null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-172.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-172.encoded.js new file mode 100644 index 00000000..f05f057e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-172.encoded.js @@ -0,0 +1,18 @@ +function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var c=new WeakMap;function A(a,g){this.g=g;this.e=a;this.o=!1;this.u=void 0}function B(a,g,k){this.q=k;this.v=a;this.h=g;this.m=null;this.g=[];this.s=1;this.n=0}function C(a,g,k){this.t=k;this.i=[];this.E=[g,a];this.prototype={}}function fa(a,g,k){g=a.g[g+7]+k;k=a.m||(a.m=[]);return k[g]||(k[g]=new A(g,a.g))} +B.prototype.d=function(a,g,k,b,h,f){a=g.t;h=this.g;for(var m=this.s,d=8+a.l,e=m+d;h.length>>13)&65535);return g}function Q(a,g,k,b){var h=a.m;if(h){var f=a.g;a.b=k||b;a=g+f[g+6];for(g=f[g+7];g=a.q.length)break;b[f+0]=m+1;m=a.q[m];try{var d=b[f+7];switch(m){case 0:var e=J(a);b[d+e]=D(a);break;case 1:e=J(a);b[d+e]=J(a);break;case 2:e=J(a);var p=D(a);if(!(p in a.h))throw new ReferenceError(`${p} is not defined`);b[d+e]=a.h[p];break;case 3:e=J(a);b[d+e]=b[f+4].i[J(a)].w(void 0);break;case 4:e=J(a);b[d+e]=b[f+3];break;case 5:e=J(a);b[d+e]=b[d+J(a)];break;case 6:a.h[D(a)]=b[d+J(a)];break;case 7:var ha=J(a); +b[f+4].i[ha].c(void 0,b[d+J(a)],null,"z");break;case 8:e=J(a);var n=b[d+J(a)],q=b[d+J(a)];b[d+e]=n[q];break;case 9:n=b[d+J(a)];q=b[d+J(a)];var x=b[d+J(a)];Reflect.set(n,q,x);break;case 10:e=J(a);n=b[d+J(a)];q=b[d+J(a)];b[d+e]=delete n[q];break;case 11:e=J(a);var l=b[d+J(a)];b[d+e]=l+b[d+J(a)];break;case 12:e=J(a);l=b[d+J(a)];b[d+e]=l-b[d+J(a)];break;case 13:e=J(a);l=b[d+J(a)];b[d+e]=l*b[d+J(a)];break;case 14:e=J(a);l=b[d+J(a)];b[d+e]=l/b[d+J(a)];break;case 15:e=J(a);l=b[d+J(a)];b[d+e]=l%b[d+J(a)]; +break;case 60:e=J(a);l=b[d+J(a)];b[d+e]=Math.pow(l,b[d+J(a)]);break;case 16:e=J(a);l=b[d+J(a)];b[d+e]=l&b[d+J(a)];break;case 17:e=J(a);l=b[d+J(a)];b[d+e]=l|b[d+J(a)];break;case 18:e=J(a);l=b[d+J(a)];b[d+e]=l^b[d+J(a)];break;case 19:e=J(a);l=b[d+J(a)];b[d+e]=l<>b[d+J(a)];break;case 21:e=J(a);l=b[d+J(a)];b[d+e]=l>>>b[d+J(a)];break;case 22:e=J(a);l=b[d+J(a)];b[d+e]=lb[d+J(a)];break;case 24:e=J(a); +l=b[d+J(a)];b[d+e]=l<=b[d+J(a)];break;case 25:e=J(a);l=b[d+J(a)];b[d+e]=l>=b[d+J(a)];break;case 26:e=J(a);l=b[d+J(a)];b[d+e]=l===b[d+J(a)];break;case 27:e=J(a);l=b[d+J(a)];b[d+e]=l!==b[d+J(a)];break;case 28:e=J(a);l=b[d+J(a)];b[d+e]=l==b[d+J(a)];break;case 29:e=J(a);l=b[d+J(a)];b[d+e]=l!=b[d+J(a)];break;case 30:e=J(a);l=b[d+J(a)];b[d+e]=l in b[d+J(a)];break;case 31:e=J(a);n=b[d+J(a)];b[d+e]=n instanceof b[d+J(a)];break;case 32:e=J(a);b[d+e]=-b[d+J(a)];break;case 33:e=J(a);b[d+e]=+b[d+J(a)];break; +case 34:e=J(a);b[d+e]=!b[d+J(a)];break;case 35:e=J(a);b[d+e]=~b[d+J(a)];break;case 36:e=J(a);b[d+e]=typeof b[d+J(a)];break;case 37:e=J(a);J(a);b[d+e]=void 0;break;case 38:e=J(a);var T=D(a);x=Object.prototype.hasOwnProperty.call(a.h,T)?a.h[T]:void 0;b[d+e]=typeof x;break;case 39:b[f+0]=J(a);break;case 40:var K=J(a),L=J(a);b[d+K]||(b[f+0]=L);break;case 41:K=J(a);L=J(a);b[d+K]&&(b[f+0]=L);break;case 42:e=J(a);var r=b[d+J(a)],t=J(a);if(t===65535)k=b[d+J(a)];else for(k=Array(t),h=0;h>1):0]=E;break;case 46:throw b[d+J(a)];case 47:e=J(a);var ja=J(a),ka=J(a),la=J(a),W=J(a),ma=J(a),N=Array(W);for(h=0;h=H.x.length?b[f+0]=sa:b[d+e]=H.x[H.B++];break;case 54:var v=b[f+5];v||(b[f+5]=v=[]);v.push({F:J(a),A:J(a)});break;case 55:b[f+5].pop();break;case 59:(v=b[f+5])||(b[f+5]=v=[]);v.push({k:J(a),C:J(a),f:J(a),D:J(a)});break;case 56:var ca=J(a),da=J(a),ta=J(a),I=J(a)^ca|0;for(f=da;f>>13)>>>0;break;case 58:b[f+0]=b[d+J(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+ +" at pc "+(b[f+0]-1));}}catch(y){f=0;for(m=a.n;m;){var w=b[m+5];if(w&&w.length>0){f=m;break}Q(a,m,[],{});a.s=m;m=b[m+1];a.n=m}if(!f)throw y;m=b[f+5].pop();w=b[f+7];m.A!==void 0?(b[w+m.A]=y,b[f+0]=m.F):(b[w+m.C]=m.D,b[w+m.f]=y,b[f+0]=m.k);a.s=f+b[f+6];a.n=f}if(!a.n)return a.g[0]}}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new B(["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],S,[4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40, +8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237,2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7]),new C(35,void 0,{j:0,l:12, +p:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-173.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-173.encoded.js new file mode 100644 index 00000000..2a683dd8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-173.encoded.js @@ -0,0 +1,18 @@ +function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}function c(a,b,f,h,m){this.z=h;this.r=m;this.h=f;this.e=null;this.g=[];this.w=b;this.c=a;this.n=1;this.q=0}var B=new WeakMap;function C(a){this.C=a;this.i=[];this.prototype={}}function D(a,b){this.g=b;this.p=a;this.a=!1;this.l=void 0}D.prototype.y=function(a,b,f,h){this.a?this.l=h:this.g[this.p]=h};function E(a,b){a.f=b;return a.z[a.g[a.q+0]++]} +function K(a,b){var f=a.e;if(f){var h=a.g;a=b+h[b+6];for(b=h[b+7];b>>13)&65535);return b} +function S(a,b,f,h,m,k,l,d){var e=m.C,p=a.g,r=a.n,n=8+e.j,q=r+n;for(a.x=h;p.length=a.z.length)break;b[f+0]=l+1;l=a.z[l];try{var d=b[f+7];switch(l){case 0:var e=E(a,{});b[d+e]=L(a);break;case 1:e=E(a,4);b[d+e]=E(a,[]);break;case 2:e=E(a,"x");var p=L(a);if(!(p in a.h))throw new ReferenceError(`${p} is not defined`);b[d+e]=a.h[p];break;case 3:e=E(a,"y");b[d+e]=b[f+4].i[E(a,"y")].E(78,[],void 0,[]);break;case 4:e=E(a,[]);b[d+e]=b[f+3];break;case 5:e=E(a,[]);b[d+e]=b[d+E(a,43)];break;case 6:a.h[L(a)]= +b[d+E(a,[])];break;case 7:var r=E(a,{});b[f+4].i[r].y([],{},[],b[d+E(a,"y")],{});break;case 8:e=E(a,null);var n=b[d+E(a,29)],q=b[d+E(a,8)];b[d+e]=n[q];break;case 9:n=b[d+E(a)];q=b[d+E(a,{})];var y=b[d+E(a,[])];Reflect.set(n,q,y);break;case 10:e=E(a,{});n=b[d+E(a,null)];q=b[d+E(a,[])];b[d+e]=delete n[q];break;case 11:e=E(a,null);var g=b[d+E(a,{})];b[d+e]=g+b[d+E(a,null)];break;case 12:e=E(a,4);g=b[d+E(a,[])];b[d+e]=g-b[d+E(a,"k")];break;case 13:e=E(a,null);g=b[d+E(a)];b[d+e]=g*b[d+E(a)];break;case 14:e= +E(a);g=b[d+E(a,74)];b[d+e]=g/b[d+E(a)];break;case 15:e=E(a,[]);g=b[d+E(a,34)];b[d+e]=g%b[d+E(a,[])];break;case 60:e=E(a,"y");g=b[d+E(a,{})];b[d+e]=Math.pow(g,b[d+E(a)]);break;case 16:e=E(a,null);g=b[d+E(a,null)];b[d+e]=g&b[d+E(a,null)];break;case 17:e=E(a,null);g=b[d+E(a)];b[d+e]=g|b[d+E(a,[])];break;case 18:e=E(a,44);g=b[d+E(a,"x")];b[d+e]=g^b[d+E(a,0)];break;case 19:e=E(a,null);g=b[d+E(a,"k")];b[d+e]=g<>b[d+E(a,null)];break;case 21:e= +E(a,62);g=b[d+E(a,[])];b[d+e]=g>>>b[d+E(a,null)];break;case 22:e=E(a,"k");g=b[d+E(a,null)];b[d+e]=gb[d+E(a,"z")];break;case 24:e=E(a,[]);g=b[d+E(a,89)];b[d+e]=g<=b[d+E(a,[])];break;case 25:e=E(a,{});g=b[d+E(a,12)];b[d+e]=g>=b[d+E(a,[])];break;case 26:e=E(a);g=b[d+E(a,[])];b[d+e]=g===b[d+E(a,[])];break;case 27:e=E(a,{});g=b[d+E(a,[])];b[d+e]=g!==b[d+E(a,14)];break;case 28:e=E(a,"z");g=b[d+E(a,{})];b[d+e]=g==b[d+E(a,[])];break;case 29:e= +E(a,[]);g=b[d+E(a,null)];b[d+e]=g!=b[d+E(a,null)];break;case 30:e=E(a);g=b[d+E(a,null)];b[d+e]=g in b[d+E(a,{})];break;case 31:e=E(a,[]);n=b[d+E(a,[])];b[d+e]=n instanceof b[d+E(a,"k")];break;case 32:e=E(a,"z");b[d+e]=-b[d+E(a,39)];break;case 33:e=E(a,[]);b[d+e]=+b[d+E(a,{})];break;case 34:e=E(a,"y");b[d+e]=!b[d+E(a,68)];break;case 35:e=E(a,null);b[d+e]=~b[d+E(a,71)];break;case 36:e=E(a,null);b[d+e]=typeof b[d+E(a,{})];break;case 37:e=E(a,"y");E(a,[]);b[d+e]=void 0;break;case 38:e=E(a);var V=L(a); +y=Object.prototype.hasOwnProperty.call(a.h,V)?a.h[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=E(a,40);break;case 40:var M=E(a),N=E(a);b[d+M]||(b[f+0]=N);break;case 41:M=E(a,[]);N=E(a,null);b[d+M]&&(b[f+0]=N);break;case 42:e=E(a);var t=b[d+E(a,null)],u=E(a,{});if(u===65535)m=b[d+E(a,[])];else for(m=Array(u),k=0;k>1):0]=F;break;case 46:throw b[d+E(a,"q")];case 47:e=E(a,{});var ja=E(a,[]),ka=E(a,17),la=E(a,[]),Y=E(a,[]),ma=E(a,null),P=Array(Y);for(k=0;k=I.o.length?b[f+0]=sa:b[d+e]=I.o[I.u++];break;case 54:var w=b[f+5];w||(b[f+5]=w=[]);w.push({D:E(a,[]),b:E(a)});break;case 55:b[f+5].pop();break;case 59:(w=b[f+5])||(b[f+5]=w=[]);w.push({A:E(a),B:E(a),H:E(a,"z"),F:E(a)});break;case 56:var ea=E(a),fa=E(a,{}),ta=E(a),J=E(a,{})^ea|0;for(f=fa;f>>13)>>>0;break;case 58:b[f+0]=b[d+E(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+ +l+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(l=a.q;l;){var x=b[l+5];if(x&&x.length>0){f=l;break}K(a,l);a.n=l;l=b[l+1];a.q=l}if(!f)throw z;l=b[f+5].pop();x=b[f+7];l.b!==void 0?(b[x+l.b]=z,b[f+0]=l.D):(b[x+l.B]=l.F,b[x+l.H]=z,b[f+0]=l.A);a.n=f+b[f+6];a.q=f}if(!a.q)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new c({},{},U,[4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7, +40,8,237,2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"]),void 0,void 0,new C({k:0, +j:12,v:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-174.encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-174.encoded.js new file mode 100644 index 00000000..eb9acc05 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/minifyClassObfuscation-174.encoded.js @@ -0,0 +1,20 @@ +function b(a,d,h,f,l,k){this.j=d;this.w=a;this.i=k;this.E=null;this.r=f;this.g=[];this.z=l&&h;this.m=1;this.h=0}function x(a){this.F=a;this.l=[];this.prototype={}}function ba(a,d){this.g=a;this.q=d;this.b=!1;this.a=void 0}function ca(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(d){return d.charCodeAt(0)})}var B=new WeakMap;function da(a){return a.b?a.a:a.g[a.q]} +b.prototype.c=function(a,d,h,f,l,k){d=a.F;l=this.g;for(var c=this.m,e=8+d.n,t=c+e;l.length>>13)&65535);return h}; +function fa(a,d){a.b?a.a=d:a.g[a.q]=d}function O(a){return a.j[a.g[a.h+0]++]} +b.prototype.t=function(a,d,h,f,l){var k;this.f=d;for(this.c(l,void 0,0,h,"x",a,void 0);;){d=this.h;a=this.g;f=a[d+0];if(f>=this.j.length)break;a[d+0]=f+1;f=this.j[f];try{var c=a[d+7];switch(f){case 0:var e=O(this);a[c+e]=this.k();break;case 1:e=O(this);a[c+e]=O(this);break;case 2:e=O(this);var t=this.k();if(!(t in this.i))throw new ReferenceError(`${t} is not defined`);a[c+e]=this.i[t];break;case 3:e=O(this);a[c+e]=da(a[d+4].l[O(this)]);break;case 4:e=O(this);a[c+e]=a[d+3];break;case 5:e=O(this); +a[c+e]=a[c+O(this)];break;case 6:this.i[this.k()]=a[c+O(this)];break;case 7:var p=O(this);fa(a[d+4].l[p],a[c+O(this)]);break;case 8:e=O(this);var m=a[c+O(this)],n=a[c+O(this)];a[c+e]=m[n];break;case 9:m=a[c+O(this)];n=a[c+O(this)];var y=a[c+O(this)];Reflect.set(m,n,y);break;case 10:e=O(this);m=a[c+O(this)];n=a[c+O(this)];a[c+e]=delete m[n];break;case 11:e=O(this);var g=a[c+O(this)];a[c+e]=g+a[c+O(this)];break;case 12:e=O(this);g=a[c+O(this)];a[c+e]=g-a[c+O(this)];break;case 13:e=O(this);g=a[c+O(this)]; +a[c+e]=g*a[c+O(this)];break;case 14:e=O(this);g=a[c+O(this)];a[c+e]=g/a[c+O(this)];break;case 15:e=O(this);g=a[c+O(this)];a[c+e]=g%a[c+O(this)];break;case 60:e=O(this);g=a[c+O(this)];a[c+e]=Math.pow(g,a[c+O(this)]);break;case 16:e=O(this);g=a[c+O(this)];a[c+e]=g&a[c+O(this)];break;case 17:e=O(this);g=a[c+O(this)];a[c+e]=g|a[c+O(this)];break;case 18:e=O(this);g=a[c+O(this)];a[c+e]=g^a[c+O(this)];break;case 19:e=O(this);g=a[c+O(this)];a[c+e]=g<>a[c+O(this)];break;case 21:e=O(this);g=a[c+O(this)];a[c+e]=g>>>a[c+O(this)];break;case 22:e=O(this);g=a[c+O(this)];a[c+e]=ga[c+O(this)];break;case 24:e=O(this);g=a[c+O(this)];a[c+e]=g<=a[c+O(this)];break;case 25:e=O(this);g=a[c+O(this)];a[c+e]=g>=a[c+O(this)];break;case 26:e=O(this);g=a[c+O(this)];a[c+e]=g===a[c+O(this)];break;case 27:e=O(this);g=a[c+O(this)];a[c+e]=g!==a[c+O(this)];break;case 28:e=O(this);g=a[c+O(this)];a[c+e]=g== +a[c+O(this)];break;case 29:e=O(this);g=a[c+O(this)];a[c+e]=g!=a[c+O(this)];break;case 30:e=O(this);g=a[c+O(this)];a[c+e]=g in a[c+O(this)];break;case 31:e=O(this);m=a[c+O(this)];a[c+e]=m instanceof a[c+O(this)];break;case 32:e=O(this);a[c+e]=-a[c+O(this)];break;case 33:e=O(this);a[c+e]=+a[c+O(this)];break;case 34:e=O(this);a[c+e]=!a[c+O(this)];break;case 35:e=O(this);a[c+e]=~a[c+O(this)];break;case 36:e=O(this);a[c+e]=typeof a[c+O(this)];break;case 37:e=O(this);O(this);a[c+e]=void 0;break;case 38:e= +O(this);var Q=this.k();y=Object.prototype.hasOwnProperty.call(this.i,Q)?this.i[Q]:void 0;a[c+e]=typeof y;break;case 39:a[d+0]=O(this);break;case 40:var I=O(this),J=O(this);a[c+I]||(a[d+0]=J);break;case 41:I=O(this);J=O(this);a[c+I]&&(a[d+0]=J);break;case 42:e=O(this);var q=a[c+O(this)],r=O(this);if(r===65535)h=a[c+O(this)];else for(h=Array(r),k=0;k>1):0]=D;break;case 46:throw a[c+O(this)];case 47:e=O(this);var ia=O(this),ja=O(this),ka=O(this),T=O(this),la=O(this),L=Array(T);for(k=0;k=G.x.length?a[d+0]=ra:a[c+e]=G.x[G.u++];break;case 54:var v=a[d+5];v||(a[d+5]=v=[]);v.push({e:O(this),A:O(this)});break;case 55:a[d+5].pop();break;case 59:(v=a[d+5])||(a[d+5]=v=[]);v.push({C:O(this),y:O(this),s:O(this),p:O(this)});break;case 56:var Z=O(this),aa=O(this),sa=O(this),H=O(this)^Z|0;for(d=aa;d>>13)>>>0;break;case 58:a[d+0]=a[c+O(this)];break;case 57:debugger;break; +default:throw Error("Unknown opcode: "+f+" at pc "+(a[d+0]-1));}}catch(z){d=0;for(f=this.h;f;){var w=a[f+5];if(w&&w.length>0){d=f;break}C(this,f);this.m=f;this.h=f=a[f+1]}if(!d)throw z;f=a[d+5].pop();w=a[d+7];f.A!==void 0?(a[w+f.A]=z,a[d+0]=f.e):(a[w+f.y]=f.p,a[w+f.s]=z,a[d+0]=f.C);this.m=d+a[d+6];this.h=d}if(!this.h)return this.g[0]}};var P=globalThis;typeof window!=="undefined"&&(P.window=window,P.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(P.module=module,P.exports=typeof exports!=="undefined"?exports:void 0); +(new b(["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],[4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185, +2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237,2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],[],{},[],P)).t(void 0,void 0,null, +void 0,new x({o:0,n:12,v:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/native-qualification.json b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/native-qualification.json new file mode 100644 index 00000000..0c936790 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/native-qualification.json @@ -0,0 +1,562 @@ +{ + "schemaVersion": "vm-native-local-fixtures.v1", + "encoderPin": "20c5b96bf57337c568579352758d7685358650de", + "sourceFile": "exception-routes.source.js", + "expectedFile": "exception-routes.expected.json", + "qualifiedAt": "2026-10-05T18:29:40.814111+00:00", + "boundary": "Frozen generated source/encoded bytes were newly observed with current public decoded output under a bounded native host. Finite fixture qualification; no global final JavaScript semantics claim.", + "generationRecordBoundary": "Generation-record digests identify supplied value data after JSON normalization; retained raw journals remain external to this package.", + "execution": { + "utcStart": "2026-10-05T18:15:26.519876+00:00", + "utcEnd": "2026-10-05T18:19:23.758137+00:00", + "node": "v26.9.0", + "platform": "Darwin", + "architecture": "arm64", + "observedExitCode": 0, + "observedSignal": null, + "summarySha256": "b617868764dcc0456dd5a6835243a6b999cee81f54c9b165697534968bf8fe02", + "populationManifestSha256": "5909fb6a110d3c020de2ebfd3800651e6245850acc34ecb4b843667b662d1b9b" + }, + "cells": [ + { + "id": "exceptionRoutes--baseline--172", + "profile": "baseline", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "baseline-172.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 172, + "state": 172 + }, + "generationRecordValueSha256": "0a38523064be996028be429839df1b142c03ac99fb058a837e81241829d30a2b", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "reference", + "observed": null + } + }, + { + "id": "exceptionRoutes--baseline--173", + "profile": "baseline", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "baseline-173.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 173, + "state": 173 + }, + "generationRecordValueSha256": "bd534ff3ae4d8ebb3fa83472ab76cbf1201ae2d595e1bd1a305dbcad6fde134c", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "reference", + "observed": null + } + }, + { + "id": "exceptionRoutes--baseline--174", + "profile": "baseline", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "baseline-174.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 174, + "state": 174 + }, + "generationRecordValueSha256": "b8fd5117819ae96ee6184f13c454836f797b42138f552b4a37cbfbbe4eaebc2f", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "reference", + "observed": null + } + }, + { + "id": "exceptionRoutes--minify--172", + "profile": "minify", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "minify-172.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 172, + "state": 172 + }, + "generationRecordValueSha256": "19e4b3cbf0da22b6f5d9cfcbc38cb40026a9a0cdbd905f45f43eb32b9d5b00cd", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "runtime-minification", + "observed": true + } + }, + { + "id": "exceptionRoutes--minify--173", + "profile": "minify", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "minify-173.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 173, + "state": 173 + }, + "generationRecordValueSha256": "8f19ab2da328340633018cedad368145645b5c3266016008b5e8571bac5ff487", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "runtime-minification", + "observed": true + } + }, + { + "id": "exceptionRoutes--minify--174", + "profile": "minify", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "minify-174.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 174, + "state": 174 + }, + "generationRecordValueSha256": "1390d7db3a193175a32433d89fba8ec8d16219d0134cbb7ef32d421887d39e29", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "runtime-minification", + "observed": true + } + }, + { + "id": "exceptionRoutes--classObfuscation--172", + "profile": "classObfuscation", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "classObfuscation-172.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 690, + "seed": 172, + "state": 1060026118 + }, + "generationRecordValueSha256": "eadf209dd3eb0e4fe0ff8acccb89aa8d57fb85abae9c799b69c40173c2c26af1", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "runtime-class-layout", + "observed": true + } + }, + { + "id": "exceptionRoutes--classObfuscation--173", + "profile": "classObfuscation", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "classObfuscation-173.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 1218, + "seed": 173, + "state": 1759133783 + }, + "generationRecordValueSha256": "de760de2b3bd061aeb12fd612e63ff53a9f5c7627d9630c80c6d14a1824d3a30", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "runtime-class-layout", + "observed": true + } + }, + { + "id": "exceptionRoutes--classObfuscation--174", + "profile": "classObfuscation", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "classObfuscation-174.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 700, + "seed": 174, + "state": 2195815066 + }, + "generationRecordValueSha256": "a4ded9b594dff2930a57da49168d3b1d528c47b237a4c04c9707a51f303b5b51", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "runtime-class-layout", + "observed": true + } + }, + { + "id": "exceptionRoutes--minifyClassObfuscation--172", + "profile": "minifyClassObfuscation", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "minifyClassObfuscation-172.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 690, + "seed": 172, + "state": 1060026118 + }, + "generationRecordValueSha256": "fb1ef8e441ee4d926aee86907e050acde0ff4bcd227d93839e06eb1ce686a936", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "emitted-representation-components", + "classWitness": { + "candidate": { + "argumentCount": 3, + "optionalSlotGuards": 0, + "pool": 0, + "words": 2 + }, + "control": { + "argumentCount": 3, + "optionalSlotGuards": 9, + "pool": 1, + "words": 0 + }, + "observed": true, + "witness": "connected-optional-slots-erased" + }, + "components": { + "classObfuscation": true, + "minify": true + }, + "observed": true + } + }, + { + "id": "exceptionRoutes--minifyClassObfuscation--173", + "profile": "minifyClassObfuscation", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "minifyClassObfuscation-173.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 1218, + "seed": 173, + "state": 1759133783 + }, + "generationRecordValueSha256": "81427cbad9cc2c9e8e29e88dd1841aeefdbd8a575a4c451fdd583fd3ec90cb76", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "emitted-representation-components", + "classWitness": { + "candidate": { + "argumentCount": 5, + "optionalSlotGuards": 0, + "pool": 4, + "words": 3 + }, + "control": { + "argumentCount": 3, + "optionalSlotGuards": 9, + "pool": 1, + "words": 0 + }, + "observed": true, + "witness": "connected-optional-slots-erased" + }, + "components": { + "classObfuscation": true, + "minify": true + }, + "observed": true + } + }, + { + "id": "exceptionRoutes--minifyClassObfuscation--174", + "profile": "minifyClassObfuscation", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "encodedFile": "minifyClassObfuscation-174.encoded.js", + "rng": { + "algorithm": "mulberry32", + "draws": 700, + "seed": 174, + "state": 2195815066 + }, + "generationRecordValueSha256": "439ced21d3e72f00435f260ca9af73214bf2f7377bc78e3da48628ac1a4c99c5", + "currentOutputSha256": "766246874908d096445d7c33620a037101968a42f24846dfb2bf14384a16364b", + "qualified": true, + "behaviorAgreement": true, + "normalizedAgreement": true, + "effect": { + "axis": "emitted-representation-components", + "classWitness": { + "candidate": { + "argumentCount": 6, + "optionalSlotGuards": 0, + "pool": 0, + "words": 1 + }, + "control": { + "argumentCount": 3, + "optionalSlotGuards": 9, + "pool": 1, + "words": 0 + }, + "observed": true, + "witness": "connected-optional-slots-erased" + }, + "components": { + "classObfuscation": true, + "minify": true + }, + "observed": true + } + } + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-admission/provenance.md b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/provenance.md new file mode 100644 index 00000000..60455f7b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-admission/provenance.md @@ -0,0 +1,20 @@ +# Exception source and qualification boundary + +This source and independent ordered result are inputs, not native qualification. +JSON.parse("{") throws SyntaxError, reading null.missing throws TypeError, and explicit Error +plus normal return exercise captured count and finally. The payload assertions require +CALL_METHOD, GET_PROP, THROW, catch/finally setup, MAKE_CLOSURE/capture, upvalue read/write and +RETURN routes before native observations of source, encoded input and current emitted output. + +The separate behavior test requires local `native-qualification.json` with sourceSha256 and twelve +cells (baseline/minify/classObfuscation/minifyClassObfuscation at seeds 172–174). Each cell names +its local encodedFile (`PROFILE-SEED.encoded.js`) and encodedSha256 and retains options, encoder revision, seed/RNG, +generation/tool/runtime/process identities and observations. Encoded files and this metadata +are retained here after separately accepted qualification. This population metadata uses a +distinct filename from the single-cell `qualification.json` records discovered by the generic +provenance validator. +Absence fails before native execution. Historical encoded fixtures are not substituted. + +The native context supplies window/self, performance, atob/btoa and TEST_OUTPUT, with no +Buffer/process/require; each invocation is limited to 10s and each test to 45s. The parser-only +admission command excludes this test and its native support module. diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-169.js b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-169.js new file mode 100644 index 00000000..8a800399 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-169.js @@ -0,0 +1,16 @@ +function c(a,f,h){this.d=a;this.A=h;this.h=f;this.l=null;this.p=[];this.c=1;this.g=0}var B=new WeakMap;function fa(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})}function C(a,f){this.f="x";this.p=a;this.m=f;this.u=!1;this.o=void 0}function D(a,f,h){this.E=h;this.s=a;this.i=[];this.prototype={};this.r=f}C.prototype.e=function(a,f,h){this.u?this.o=h:this.p[this.m]=h};C.prototype.Q=function(){return this.u?this.o:this.p[this.m]}; +c.prototype.k=function(a,f,h){a=h??J(this);f=f??J(this);a=this.A[a];if(!f)return a;if(typeof a==="number")return a^f;if(typeof a!=="string")return a;a=fa(a);h="";for(var b=0;b>>13)&65535);return h};function ha(a,f,h){f=a.p[f+7]+h;h=a.l||(a.l=[]);return h[f]||(h[f]=new C(a.p,f))}function J(a){return a.d[a.p[a.g+0]++]} +function K(a,f,h,b,l,g){for(var m=l.s,d=a.p,e=a.c,u=8+m.z,x=e+u;d.length=a.d.length)break;b[g+0]=m+1;m=a.d[m];try{var d=b[g+7];switch(m){case 0:var e=J(a);b[d+e]=a.k();break;case 1:e=J(a);b[d+e]=J(a);break;case 2:e=J(a);var u=a.k();if(!(u in a.h))throw new ReferenceError(`${u} is not defined`);b[d+e]=a.h[u];break;case 3:e=J(a);b[d+e]=b[g+4].i[J(a)].Q(void 0,void 0,[],14);break;case 4:e=J(a);b[d+e]=b[g+3];break;case 5:e=J(a);b[d+e]=b[d+J(a)];break;case 6:a.h[a.k()]=b[d+J(a)];break;case 7:var x= +J(a);b[g+4].i[x].e(21,void 0,b[d+J(a)]);break;case 8:e=J(a);var n=b[d+J(a)],p=b[d+J(a)];b[d+e]=n[p];break;case 9:n=b[d+J(a)];p=b[d+J(a)];var y=b[d+J(a)];Reflect.set(n,p,y);break;case 10:e=J(a);n=b[d+J(a)];p=b[d+J(a)];b[d+e]=delete n[p];break;case 11:e=J(a);var k=b[d+J(a)];b[d+e]=k+b[d+J(a)];break;case 12:e=J(a);k=b[d+J(a)];b[d+e]=k-b[d+J(a)];break;case 13:e=J(a);k=b[d+J(a)];b[d+e]=k*b[d+J(a)];break;case 14:e=J(a);k=b[d+J(a)];b[d+e]=k/b[d+J(a)];break;case 15:e=J(a);k=b[d+J(a)];b[d+e]=k%b[d+J(a)];break; +case 60:e=J(a);k=b[d+J(a)];b[d+e]=Math.pow(k,b[d+J(a)]);break;case 16:e=J(a);k=b[d+J(a)];b[d+e]=k&b[d+J(a)];break;case 17:e=J(a);k=b[d+J(a)];b[d+e]=k|b[d+J(a)];break;case 18:e=J(a);k=b[d+J(a)];b[d+e]=k^b[d+J(a)];break;case 19:e=J(a);k=b[d+J(a)];b[d+e]=k<>b[d+J(a)];break;case 21:e=J(a);k=b[d+J(a)];b[d+e]=k>>>b[d+J(a)];break;case 22:e=J(a);k=b[d+J(a)];b[d+e]=kb[d+J(a)];break;case 24:e=J(a);k=b[d+ +J(a)];b[d+e]=k<=b[d+J(a)];break;case 25:e=J(a);k=b[d+J(a)];b[d+e]=k>=b[d+J(a)];break;case 26:e=J(a);k=b[d+J(a)];b[d+e]=k===b[d+J(a)];break;case 27:e=J(a);k=b[d+J(a)];b[d+e]=k!==b[d+J(a)];break;case 28:e=J(a);k=b[d+J(a)];b[d+e]=k==b[d+J(a)];break;case 29:e=J(a);k=b[d+J(a)];b[d+e]=k!=b[d+J(a)];break;case 30:e=J(a);k=b[d+J(a)];b[d+e]=k in b[d+J(a)];break;case 31:e=J(a);n=b[d+J(a)];b[d+e]=n instanceof b[d+J(a)];break;case 32:e=J(a);b[d+e]=-b[d+J(a)];break;case 33:e=J(a);b[d+e]=+b[d+J(a)];break;case 34:e= +J(a);b[d+e]=!b[d+J(a)];break;case 35:e=J(a);b[d+e]=~b[d+J(a)];break;case 36:e=J(a);b[d+e]=typeof b[d+J(a)];break;case 37:e=J(a);J(a);b[d+e]=void 0;break;case 38:e=J(a);var U=a.k();y=Object.prototype.hasOwnProperty.call(a.h,U)?a.h[U]:void 0;b[d+e]=typeof y;break;case 39:b[g+0]=J(a);break;case 40:var L=J(a),M=J(a);b[d+L]||(b[g+0]=M);break;case 41:L=J(a);M=J(a);b[d+L]&&(b[g+0]=M);break;case 42:e=J(a);var q=b[d+J(a)],r=J(a);if(r===65535)f=b[d+J(a)];else for(f=Array(r),l=0;l>1):0]=E;break;case 46:throw b[d+J(a)];case 47:e=J(a);var ja=J(a),ka=J(a),la=J(a),X=J(a),ma=J(a),O=Array(X);for(l=0;l=H.D.length?b[g+0]=sa:b[d+e]=H.D[H.v++];break;case 54:var v=b[g+5];v||(b[g+5]=v=[]);v.push({a:J(a),B:J(a)});break;case 55:b[g+5].pop();break;case 59:(v=b[g+5])||(b[g+5]=v=[]);v.push({b:J(a),J:J(a),I:J(a),H:J(a)});break;case 56:var da=J(a),ea=J(a),ta=J(a),I=J(a)^da|0;for(g=ea;g>>13)>>>0;break;case 58:b[g+0]=b[d+J(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+" at pc "+(b[g+0]- +1));}}catch(z){g=0;for(m=a.g;m;){var w=b[m+5];if(w&&w.length>0){g=m;break}R(a,m);a.c=m;m=b[m+1];a.g=m}if(!g)throw z;m=b[g+5].pop();w=b[g+7];m.B!==void 0?(b[w+m.B]=z,b[g+0]=m.a):(b[w+m.J]=m.H,b[w+m.I]=z,b[g+0]=m.b);a.c=g+b[g+6];a.g=g}if(!a.g)return a.p[0]}}var T=globalThis;typeof window!=="undefined"&&(T.window=window,T.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(T.module=module,T.exports=typeof exports!=="undefined"?exports:void 0); +S(new c([4,1,2,2,0,0,0,3,1,0,0,4,2,0,0,5,3,0,13,6,4,5,0,4,4,0,11,5,6,4,0,4,5,0,15,6,5,4,9,2,3,6,0,2,6,0,45,2],T,["window","TEST_OUTPUT",6,7,3,5,void 0]),null,new D({j:0,z:7,C:0},null,[])); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-170.js b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-170.js new file mode 100644 index 00000000..a6113a1a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-170.js @@ -0,0 +1,17 @@ +function b(a,f,e,l,k){this.h=l;this.C=a;this.E=e;this.g=k;this.t=null;this.f=[];this.z=f;this.j=1;this.s=0}var A=new WeakMap;function B(a){this.G=a;this.i=[];this.prototype={}}function C(a,f){this.f=f;this.o=a;this.u=!1;this.q=void 0}function da(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})} +b.prototype.w=function(a){var f=this.t;if(f){var e=this.f,l=a+e[a+6];for(a=e[a+7];a>>13)&65535);return l}function ea(a,f){a.u?a.q=f:a.f[a.o]=f}function fa(a,f,e){a.x="q";f=a.f[f+7]+e;e=a.t||(a.t=[]);return e[f]||(e[f]=new C(f,a.f))} +b.prototype.e=function(a,f,e,l,k,p,n){var g;this.n=e;for(D(this,0,l,n,f);;){e=this.s;a=this.f;k=a[e+0];if(k>=this.h.length)break;a[e+0]=k+1;k=this.h[k];try{var c=a[e+7];switch(k){case 0:var d=E(this);a[c+d]=Q(this);break;case 1:d=E(this);a[c+d]=E(this);break;case 2:d=E(this);var q=Q(this);if(!(q in this.g))throw new ReferenceError(`${q} is not defined`);a[c+d]=this.g[q];break;case 3:d=E(this);a[c+d]=a[e+4].i[E(this)].r(void 0,null,"x");break;case 4:d=E(this);a[c+d]=a[e+3];break;case 5:d=E(this);a[c+ +d]=a[c+E(this)];break;case 6:this.g[Q(this)]=a[c+E(this)];break;case 7:var ha=E(this);ea(a[e+4].i[ha],a[c+E(this)]);break;case 8:d=E(this);var m=a[c+E(this)],r=a[c+E(this)];a[c+d]=m[r];break;case 9:m=a[c+E(this)];r=a[c+E(this)];var x=a[c+E(this)];Reflect.set(m,r,x);break;case 10:d=E(this);m=a[c+E(this)];r=a[c+E(this)];a[c+d]=delete m[r];break;case 11:d=E(this);var h=a[c+E(this)];a[c+d]=h+a[c+E(this)];break;case 12:d=E(this);h=a[c+E(this)];a[c+d]=h-a[c+E(this)];break;case 13:d=E(this);h=a[c+E(this)]; +a[c+d]=h*a[c+E(this)];break;case 14:d=E(this);h=a[c+E(this)];a[c+d]=h/a[c+E(this)];break;case 15:d=E(this);h=a[c+E(this)];a[c+d]=h%a[c+E(this)];break;case 60:d=E(this);h=a[c+E(this)];a[c+d]=Math.pow(h,a[c+E(this)]);break;case 16:d=E(this);h=a[c+E(this)];a[c+d]=h&a[c+E(this)];break;case 17:d=E(this);h=a[c+E(this)];a[c+d]=h|a[c+E(this)];break;case 18:d=E(this);h=a[c+E(this)];a[c+d]=h^a[c+E(this)];break;case 19:d=E(this);h=a[c+E(this)];a[c+d]=h<>a[c+E(this)];break;case 21:d=E(this);h=a[c+E(this)];a[c+d]=h>>>a[c+E(this)];break;case 22:d=E(this);h=a[c+E(this)];a[c+d]=ha[c+E(this)];break;case 24:d=E(this);h=a[c+E(this)];a[c+d]=h<=a[c+E(this)];break;case 25:d=E(this);h=a[c+E(this)];a[c+d]=h>=a[c+E(this)];break;case 26:d=E(this);h=a[c+E(this)];a[c+d]=h===a[c+E(this)];break;case 27:d=E(this);h=a[c+E(this)];a[c+d]=h!==a[c+E(this)];break;case 28:d=E(this);h=a[c+E(this)];a[c+d]=h== +a[c+E(this)];break;case 29:d=E(this);h=a[c+E(this)];a[c+d]=h!=a[c+E(this)];break;case 30:d=E(this);h=a[c+E(this)];a[c+d]=h in a[c+E(this)];break;case 31:d=E(this);m=a[c+E(this)];a[c+d]=m instanceof a[c+E(this)];break;case 32:d=E(this);a[c+d]=-a[c+E(this)];break;case 33:d=E(this);a[c+d]=+a[c+E(this)];break;case 34:d=E(this);a[c+d]=!a[c+E(this)];break;case 35:d=E(this);a[c+d]=~a[c+E(this)];break;case 36:d=E(this);a[c+d]=typeof a[c+E(this)];break;case 37:d=E(this);E(this);a[c+d]=void 0;break;case 38:d= +E(this);var S=Q(this);x=Object.prototype.hasOwnProperty.call(this.g,S)?this.g[S]:void 0;a[c+d]=typeof x;break;case 39:a[e+0]=E(this);break;case 40:var K=E(this),L=E(this);a[c+K]||(a[e+0]=L);break;case 41:K=E(this);L=E(this);a[c+K]&&(a[e+0]=L);break;case 42:d=E(this);var t=a[c+E(this)],u=E(this);if(u===65535)f=a[c+E(this)];else for(f=Array(u),g=0;g>1):0]=F;break;case 46:throw a[c+E(this)];case 47:d=E(this);var ja=E(this),ka=E(this),la=E(this),V=E(this),ma=E(this),N=Array(V);for(g=0;g=I.a.length?a[e+0]=sa:a[c+d]=I.a[I.B++];break;case 54:var w=a[e+5];w||(a[e+5]=w=[]);w.push({K:E(this),A:E(this)});break;case 55:a[e+5].pop();break;case 59:(w=a[e+5])||(a[e+5]=w=[]);w.push({J:E(this),c:E(this),F:E(this),H:E(this)});break;case 56:var ba=E(this),ca=E(this),ta=E(this),J=E(this)^ba|0;for(e=ca;e>>13)>>>0;break;case 58:a[e+0]=a[c+E(this)];break;case 57:debugger; +break;default:throw Error("Unknown opcode: "+k+" at pc "+(a[e+0]-1));}}catch(y){e=0;for(k=this.s;k;){if((p=a[k+5])&&p.length>0){e=k;break}this.w(k,void 0,17);this.j=k;this.s=k=a[k+1]}if(!e)throw y;k=a[e+5].pop();p=a[e+7];k.A!==void 0?(a[p+k.A]=y,a[e+0]=k.K):(a[p+k.c]=k.H,a[p+k.F]=y,a[e+0]=k.J);this.j=e+a[e+6];this.s=e}if(!this.s)return this.f[0]}};var R=globalThis;typeof window!=="undefined"&&(R.window=window,R.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(R.module=module,R.exports=typeof exports!=="undefined"?exports:void 0);(new b(["window","TEST_OUTPUT",6,7,3,5,void 0],null,"q",[4,1,2,2,0,0,0,3,1,0,0,4,2,0,0,5,3,0,13,6,4,5,0,4,4,0,11,5,6,4,0,4,5,0,15,6,5,4,9,2,3,6,0,2,6,0,45,2],R)).e("k",null,[],new B({m:0,l:7,v:0}),{},"k",void 0); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-171.js b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-171.js new file mode 100644 index 00000000..fecab492 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-171.js @@ -0,0 +1,16 @@ +function c(a,b,h,f){this.i=a;this.a=b;this.C=f;this.p=h;this.A=null;this.g=[];this.l=1;this.h=0}var B=new WeakMap;function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}function fa(a,b){this.g=a;this.v=b;this.u=!1;this.B=void 0}function C(a,b){this.e=b;this.j=[];this.prototype={};this.d=a} +c.prototype.c=function(a,b,h,f,l,m,k){f=m.e;l=this.g;for(var d=this.l,e=8+f.m,u=d+e;l.length>>13)&65535);return b}function ha(a,b,h){b=a.g[h+7]+b;h=a.A||(a.A=[]);return h[b]||(h[b]=new fa(a.g,b))}function ia(a){return a.u?a.B:a.g[a.v]}function Q(a,b){var h=a.A;if(h){var f=a.g;a=b+f[b+6];for(b=f[b+7];b=a.i.length)break;b[f+0]=m+1;m=a.i[m];try{var d=b[f+7];switch(m){case 0:var e=J(a);b[d+e]=D(a);break;case 1:e=J(a);b[d+e]=J(a);break;case 2:e=J(a);var u=D(a);if(!(u in a.p))throw new ReferenceError(`${u} is not defined`);b[d+e]=a.p[u];break;case 3:e=J(a);b[d+e]=ia(b[f+4].j[J(a)]);break;case 4:e=J(a);b[d+e]=b[f+3];break;case 5:e=J(a);b[d+e]=b[d+J(a)];break;case 6:a.p[D(a)]=b[d+J(a)];break;case 7:var q= +J(a);ja(b[f+4].j[q],b[d+J(a)]);break;case 8:e=J(a);var n=b[d+J(a)],p=b[d+J(a)];b[d+e]=n[p];break;case 9:n=b[d+J(a)];p=b[d+J(a)];var y=b[d+J(a)];Reflect.set(n,p,y);break;case 10:e=J(a);n=b[d+J(a)];p=b[d+J(a)];b[d+e]=delete n[p];break;case 11:e=J(a);var g=b[d+J(a)];b[d+e]=g+b[d+J(a)];break;case 12:e=J(a);g=b[d+J(a)];b[d+e]=g-b[d+J(a)];break;case 13:e=J(a);g=b[d+J(a)];b[d+e]=g*b[d+J(a)];break;case 14:e=J(a);g=b[d+J(a)];b[d+e]=g/b[d+J(a)];break;case 15:e=J(a);g=b[d+J(a)];b[d+e]=g%b[d+J(a)];break;case 60:e= +J(a);g=b[d+J(a)];b[d+e]=Math.pow(g,b[d+J(a)]);break;case 16:e=J(a);g=b[d+J(a)];b[d+e]=g&b[d+J(a)];break;case 17:e=J(a);g=b[d+J(a)];b[d+e]=g|b[d+J(a)];break;case 18:e=J(a);g=b[d+J(a)];b[d+e]=g^b[d+J(a)];break;case 19:e=J(a);g=b[d+J(a)];b[d+e]=g<>b[d+J(a)];break;case 21:e=J(a);g=b[d+J(a)];b[d+e]=g>>>b[d+J(a)];break;case 22:e=J(a);g=b[d+J(a)];b[d+e]=gb[d+J(a)];break;case 24:e=J(a);g=b[d+J(a)];b[d+ +e]=g<=b[d+J(a)];break;case 25:e=J(a);g=b[d+J(a)];b[d+e]=g>=b[d+J(a)];break;case 26:e=J(a);g=b[d+J(a)];b[d+e]=g===b[d+J(a)];break;case 27:e=J(a);g=b[d+J(a)];b[d+e]=g!==b[d+J(a)];break;case 28:e=J(a);g=b[d+J(a)];b[d+e]=g==b[d+J(a)];break;case 29:e=J(a);g=b[d+J(a)];b[d+e]=g!=b[d+J(a)];break;case 30:e=J(a);g=b[d+J(a)];b[d+e]=g in b[d+J(a)];break;case 31:e=J(a);n=b[d+J(a)];b[d+e]=n instanceof b[d+J(a)];break;case 32:e=J(a);b[d+e]=-b[d+J(a)];break;case 33:e=J(a);b[d+e]=+b[d+J(a)];break;case 34:e=J(a);b[d+ +e]=!b[d+J(a)];break;case 35:e=J(a);b[d+e]=~b[d+J(a)];break;case 36:e=J(a);b[d+e]=typeof b[d+J(a)];break;case 37:e=J(a);J(a);b[d+e]=void 0;break;case 38:e=J(a);var T=D(a);y=Object.prototype.hasOwnProperty.call(a.p,T)?a.p[T]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=J(a);break;case 40:var K=J(a),L=J(a);b[d+K]||(b[f+0]=L);break;case 41:K=J(a);L=J(a);b[d+K]&&(b[f+0]=L);break;case 42:e=J(a);var r=b[d+J(a)],t=J(a);if(t===65535)l=b[d+J(a)];else for(l=Array(t),k=0;k>1):0]=E;break;case 46:throw b[d+J(a)];case 47:e=J(a);var la=J(a),ma=J(a),na=J(a),W=J(a),oa=J(a),N=Array(W);for(k=0;k=H.k.length?b[f+0]=ua:b[d+e]=H.k[H.b++];break;case 54:var w=b[f+5];w||(b[f+5]=w=[]);w.push({F:J(a),r:J(a)});break;case 55:b[f+5].pop();break;case 59:(w=b[f+5])||(b[f+5]=w=[]);w.push({q:J(a),Q:J(a),x:J(a),z:J(a)});break;case 56:var ca=J(a),da=J(a),va=J(a),I=J(a)^ca|0;for(f=da;f>>13)>>>0;break;case 58:b[f+0]=b[d+J(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+ +" at pc "+(b[f+0]-1));}}catch(z){f=0;for(m=a.h;m;){var x=b[m+5];if(x&&x.length>0){f=m;break}Q(a,m);a.l=m;m=b[m+1];a.h=m}if(!f)throw z;m=b[f+5].pop();x=b[f+7];m.r!==void 0?(b[x+m.r]=z,b[f+0]=m.F):(b[x+m.Q]=m.z,b[x+m.x]=z,b[f+0]=m.q);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new c([4,1,2,2,0,0,0,3,1,0,0,4,2,0,0,5,3,0,13,6,4,5,0,4,4,0,11,5,6,4,0,4,5,0,15,6,5,4,9,2,3,6,0,2,6,0,45,2],54,S,["window","TEST_OUTPUT",6,7,3,5,void 0]),void 0,new C("y",{o:0,m:7,D:0}),"y",null,72); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-169.json b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-169.json new file mode 100644 index 00000000..9a95249b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-169.json @@ -0,0 +1,61 @@ +{ + "id": "arithmetic--minifyClassObfuscation--169", + "case": "arithmetic", + "profile": "minifyClassObfuscation", + "seed": 169, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": true, + "handlerTable": false, + "minify": true, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "container-declined", + "message": "extractContainerFromSource declined the input container" + }, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": null, + "effectObservation": { + "axis": "emitted-representation-components", + "observed": null, + "components": { + "minify": null, + "classObfuscation": null + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-170.json b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-170.json new file mode 100644 index 00000000..523e245b --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-170.json @@ -0,0 +1,61 @@ +{ + "id": "arithmetic--minifyClassObfuscation--170", + "case": "arithmetic", + "profile": "minifyClassObfuscation", + "seed": 170, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": true, + "handlerTable": false, + "minify": true, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "container-declined", + "message": "extractContainerFromSource declined the input container" + }, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": null, + "effectObservation": { + "axis": "emitted-representation-components", + "observed": null, + "components": { + "minify": null, + "classObfuscation": null + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-171.json b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-171.json new file mode 100644 index 00000000..ca311cff --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/qualification-171.json @@ -0,0 +1,61 @@ +{ + "id": "arithmetic--minifyClassObfuscation--171", + "case": "arithmetic", + "profile": "minifyClassObfuscation", + "seed": 171, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": true, + "handlerTable": false, + "minify": true, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "container-declined", + "message": "extractContainerFromSource declined the input container" + }, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": null, + "effectObservation": { + "axis": "emitted-representation-components", + "observed": null, + "components": { + "minify": null, + "classObfuscation": null + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-boot/source.js b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-boot/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--172.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--172.js new file mode 100644 index 00000000..67c678fb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--172.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 13, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--173.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--173.js new file mode 100644 index 00000000..67c678fb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--173.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 13, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--174.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--174.js new file mode 100644 index 00000000..67c678fb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--baseline--174.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 13, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--172.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--172.js new file mode 100644 index 00000000..41977a14 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--172.js @@ -0,0 +1,952 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(absSlot, fake_1, regs) { + this.i = fake_1; + this.g = regs; // shared reference to VM._regs flat array + this.e = absSlot; // absolute index; stable as long as frame is alive + this.I = false; + this.P = undefined; +} +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(constants, fake_13, globals, bytecode) { + this.q = bytecode; + this.K = constants; + this.u = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.F = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.W = fake_13; + this.g = []; + this.s = 1; + this.n = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +// Closure & Frame +function Closure(fake_11, fake_9, fake_10, fake_12, fn) { + this.t = fn; + this.Z = []; + this.E = [fake_10, fake_11]; + this.prototype = {}; // <- default prototype object for `new` + this.aa = fake_9 || fake_12; +} +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 13, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +VM.prototype.R = function (fp, fake_18, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.g[fp + 7] + slot; + var uvs = this.F || (this.F = []); + this.U = fake_18; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, null, this.g)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.d = function (fake_16, closure, retDst, args, fake_17, thisVal) { + var fn = closure.t; + var regs = this.g; + var fp = this.s; + var size = 8 + fn.l; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 8; + regs[fp + 0] = fn.p; + regs[fp + 1] = this.n; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.T; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.p; + this.D = fake_17 && fake_16; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.s = end; + if (args) { + if (fn.r) { + var restSlot = fn.T - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.l; a++) regs[base + a] = args[a]; + } + if (fn.T < fn.l) regs[base + fn.T] = args; + } + this.n = fp; +}; +Upvalue.prototype.c = function (fake_4, v, fake_5, fake_3) { + this.m = fake_5; + if (this.I) this.P = v;else this.g[this.e] = v; + this.z = fake_3 || fake_4; +}; +VM.prototype.M = function (fake_20, idxIn, keyIn, fake_19) { + var idx = idxIn ?? this.N("k", "z"); + var key = keyIn ?? this.N(undefined, null); + var v = this.K[idx]; + this.Y = fake_20; + this.S = fake_19; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.B = function (fp, fake_21, fake_22) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.F; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this.g; + this.b = fake_21 || fake_22; + var hi = fp + regs[fp + 6]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.a("y", {}, null); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.a = function (fake_6, fake_7, fake_8) { + this.o = fake_6; + this.P = this.g[this.e]; + this.I = true; + this.Q = fake_7; + this.h = fake_8; +}; +Upvalue.prototype.w = function (fake_2) { + this.j = fake_2; + return this.I ? this.P : this.g[this.e]; +}; +VM.prototype.N = function (fake_15, fake_14) { + this.L = fake_15 || fake_14; + return this.q[this.g[this.n + 0]++]; +}; +VM.prototype.O = function (fake_23, fake_25, closure, args, thisVal, fake_24) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.G = fake_24; + this.v = [fake_23, fake_25]; + this.d(null, closure, 0, args, "y", thisVal); + while (true) { + var fp = this.n; + var regs = this.g; + var bc = this.q; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.q[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.q.length; i++) this.q[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.s; i2++) this.g[i2] = undefined; + op = 39; + regs[fp + 0] = this.q.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 0: + { + var dst = this.N(42, 53); + regs[base + dst] = this.M(); + break; + } + case 1: + { + var dst = this.N(null, undefined); + regs[base + dst] = this.N([], undefined); + break; + } + case 2: + { + var dst = this.N([], null); + var globalName = this.M(); + if (!(globalName in this.u)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.u[globalName]; + break; + } + case 3: + { + var dst = this.N({}, 55); + regs[base + dst] = regs[fp + 4].Z[this.N("z", {})].w(undefined); + break; + } + case 4: + { + var dst = this.N(undefined, "x"); + regs[base + dst] = regs[fp + 3]; + break; + } + case 5: + { + var dst = this.N(null, null); + regs[base + dst] = regs[base + this.N({}, 7)]; + break; + } + case 6: + { + // globals[globalName] = regs[src] + this.u[this.M()] = regs[base + this.N(undefined, [])]; + break; + } + case 7: + { + var uvIdx = this.N("z", 95); + regs[fp + 4].Z[uvIdx].c(undefined, regs[base + this.N(50, [])], null, "z"); + break; + } + case 8: + { + // dst = regs[obj][regs[key]] + var dst = this.N({}, []); + var obj = regs[base + this.N("q", undefined)]; + var key = regs[base + this.N(undefined, undefined)]; + regs[base + dst] = obj[key]; + break; + } + case 9: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.N({}, "k")]; + var key = regs[base + this.N(null, 58)]; + var val = regs[base + this.N(null, {})]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 10: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.N([], {}); + var obj = regs[base + this.N(undefined, "q")]; + var key = regs[base + this.N({}, 90)]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 11: + { + var dst = this.N(undefined, "k"); + var a = regs[base + this.N([], null)]; + regs[base + dst] = a + regs[base + this.N([], 53)]; + break; + } + case 12: + { + var dst = this.N([], "y"); + var a = regs[base + this.N({}, {})]; + regs[base + dst] = a - regs[base + this.N(undefined, null)]; + break; + } + case 13: + { + var dst = this.N(undefined, {}); + var a = regs[base + this.N(undefined, undefined)]; + regs[base + dst] = a * regs[base + this.N("z", "k")]; + break; + } + case 14: + { + var dst = this.N("x", 4); + var a = regs[base + this.N({}, "q")]; + regs[base + dst] = a / regs[base + this.N(26, [])]; + break; + } + case 15: + { + var dst = this.N([], null); + var a = regs[base + this.N([], undefined)]; + regs[base + dst] = a % regs[base + this.N({}, "q")]; + break; + } + case 60: + { + // Math.pow instead of `**` + var dst = this.N({}, 75); + var a = regs[base + this.N({}, 82)]; + regs[base + dst] = Math.pow(a, regs[base + this.N({}, null)]); + break; + } + case 16: + { + var dst = this.N("z", []); + var a = regs[base + this.N(null, {})]; + regs[base + dst] = a & regs[base + this.N(41, 80)]; + break; + } + case 17: + { + var dst = this.N({}, undefined); + var a = regs[base + this.N("y", [])]; + regs[base + dst] = a | regs[base + this.N([], {})]; + break; + } + case 18: + { + var dst = this.N(null, 16); + var a = regs[base + this.N("x", {})]; + regs[base + dst] = a ^ regs[base + this.N(null, null)]; + break; + } + case 19: + { + var dst = this.N(47, []); + var a = regs[base + this.N("z", [])]; + regs[base + dst] = a << regs[base + this.N([], [])]; + break; + } + case 20: + { + var dst = this.N({}, 37); + var a = regs[base + this.N(undefined, 53)]; + regs[base + dst] = a >> regs[base + this.N(10, {})]; + break; + } + case 21: + { + var dst = this.N({}, "x"); + var a = regs[base + this.N(undefined, [])]; + regs[base + dst] = a >>> regs[base + this.N(null, [])]; + break; + } + + // Comparison (dst, src1, src2) + case 22: + { + var dst = this.N([], undefined); + var a = regs[base + this.N(undefined, undefined)]; + regs[base + dst] = a < regs[base + this.N(null, 42)]; + break; + } + case 23: + { + var dst = this.N({}, null); + var a = regs[base + this.N("x", undefined)]; + regs[base + dst] = a > regs[base + this.N(null, 16)]; + break; + } + case 24: + { + var dst = this.N(null, 26); + var a = regs[base + this.N("x", 21)]; + regs[base + dst] = a <= regs[base + this.N("z", {})]; + break; + } + case 25: + { + var dst = this.N({}, 34); + var a = regs[base + this.N({}, null)]; + regs[base + dst] = a >= regs[base + this.N(undefined, {})]; + break; + } + case 26: + { + var dst = this.N(91, null); + var a = regs[base + this.N([], {})]; + regs[base + dst] = a === regs[base + this.N(null, {})]; + break; + } + case 27: + { + var dst = this.N(60, "x"); + var a = regs[base + this.N(10, {})]; + regs[base + dst] = a !== regs[base + this.N(3, null)]; + break; + } + case 28: + { + var dst = this.N(undefined, {}); + var a = regs[base + this.N({}, null)]; + regs[base + dst] = a == regs[base + this.N({}, "k")]; + break; + } + case 29: + { + var dst = this.N([], "z"); + var a = regs[base + this.N(52, "y")]; + regs[base + dst] = a != regs[base + this.N("z", 40)]; + break; + } + case 30: + { + var dst = this.N(11, null); + var a = regs[base + this.N({}, null)]; + regs[base + dst] = a in regs[base + this.N("q", null)]; + break; + } + case 31: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.N("x", []); + var obj = regs[base + this.N(null, undefined)]; + regs[base + dst] = obj instanceof regs[base + this.N(1, null)]; + break; + } + + // Unary (dst, src) + case 32: + { + var dst = this.N("y", []); + regs[base + dst] = -regs[base + this.N(76, [])]; + break; + } + case 33: + { + var dst = this.N(undefined, undefined); + regs[base + dst] = +regs[base + this.N(null, {})]; + break; + } + case 34: + { + var dst = this.N([], {}); + regs[base + dst] = !regs[base + this.N(91, 87)]; + break; + } + case 35: + { + var dst = this.N(null, "k"); + regs[base + dst] = ~regs[base + this.N(19, {})]; + break; + } + case 36: + { + var dst = this.N(44, 19); + regs[base + dst] = typeof regs[base + this.N([], {})]; + break; + } + case 37: + { + var dst = this.N("q", "x"); + this.N("k", undefined); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 38: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.N("x", null); + var name = this.M(); + var val = Object.prototype.hasOwnProperty.call(this.u, name) ? this.u[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39: + regs[fp + 0] = this.N({}, 89); + break; + case 40: + { + var src = this.N(undefined, null); + var target = this.N({}, []); + if (!regs[base + src]) regs[fp + 0] = target; + break; + } + case 41: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.N({}, null); + var target = this.N({}, null); + if (regs[base + src]) regs[fp + 0] = target; + break; + } + + // Calls + case 42: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.N("k", undefined); + var callee = regs[base + this.N({}, "z")]; + var argc = this.N([], 30); + var args; + if (argc === 65535) { + args = regs[base + this.N([], [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.N(null, [])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.d("q", closure, dst << 1, args, "q", this.u); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 43: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.N("k", []); + var receiver = regs[base + this.N(null, "y")]; + var callee = regs[base + this.N([], "k")]; + var argc = this.N([], null); + var args; + if (argc === 65535) { + args = regs[base + this.N([], null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.N(31, {})]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.d("z", closure, dst << 1, args, "z", receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 44: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.N([], "k"); + var callee = regs[base + this.N({}, 51)]; + var argc = this.N(63, []); + var args; + if (argc === 65535) { + args = regs[base + this.N(undefined, 78)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.N(69, null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.d(undefined, closure, dst << 1 | 1, args, [], Object.create(closure.prototype || null)); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 45: + { + var retVal = regs[base + this.N(null, "q")]; + this.B(fp, "k", []); // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.s = fp; + this.n = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 46: + throw regs[base + this.N([], null)]; + + // Closures + case 47: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.N("y", {}); + var startPc = this.N({}, null); + var paramCount = this.N(55, "x"); + var regCount = this.N(12, 29); + var uvCount = this.N([], "q"); + var hasRest = this.N(38, undefined); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.N(null, undefined); + var uvIndex = this.N({}, 2); + uvDescs[i] = { + J: isLocalRaw, + y: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + T: paramCount, + l: regCount, + p: startPc, + r: hasRest + }; + var closure = new Closure(51, null, [], 59, fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.J) { + closure.Z.push(this.R(fp, {}, uvd.y)); + } else { + closure.Z.push(regs[fp + 4].Z[uvd.y]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.K, undefined, self.u, self.q).O([], {}, c, Array.prototype.slice.call(arguments), this == null ? self.u : this, 45); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 48: + { + // dst, count, [elemReg...] + var dst = this.N([], undefined); + var count = this.N(undefined, "y"); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.N({}, [])]; + regs[base + dst] = elems; + break; + } + case 49: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.N({}, "z"); + var pairCount = this.N({}, null); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.N(null, 56)]; + var val = regs[base + this.N("k", undefined)]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50: + { + // obj, key, fn + var obj = regs[base + this.N([], null)]; + var key = regs[base + this.N([], 72)]; + var getterFn = regs[base + this.N({}, [])]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 51: + { + // obj, key, fn + var obj = regs[base + this.N(82, undefined)]; + var key = regs[base + this.N([], null)]; + var setterFn = regs[base + this.N("z", 88)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.N(22, null); + var obj = regs[base + this.N(undefined, undefined)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + x: keys, + X: 0 + }; + break; + } + case 53: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.N(4, undefined); + var iter = regs[base + this.N(58, "y")]; + var exitTarget = this.N("y", []); + if (iter.X >= iter.x.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.x[iter.X++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 54: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + H: this.N(2, []), + V: this.N("k", []) + }); + break; + } + case 55: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 59: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + k: this.N(undefined, 89), + A: this.N([], {}), + f: this.N("x", 31), + C: this.N("x", {}) + }); + break; + } + + // Self-modifying bytecode + case 56: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.N(null, undefined); + var sliceStart = this.N(null, undefined); + var sliceEnd = this.N(38, []); + var pk = this.N({}, "k") ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.q[destPc + (pi - sliceStart)] = (this.q[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 58: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.N(59, null)]; + break; + } + case 57: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 0] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.n; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.B(searchFrame, [], {}); + this.s = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.n = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.V !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.V] = err; + regs[handledFrame + 0] = h.H; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.A] = h.C; + regs[hBase + h.f] = err; + regs[handledFrame + 0] = h.k; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.s = handledFrame + regs[handledFrame + 6]; + this.n = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.n) return this.g[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(CONSTANTS, "x", globals, decodeBytecode(BYTECODE)); +vm.O("k", {}, new Closure(35, [], undefined, undefined, { + T: 0, + l: 7, + p: 0 +}), null // no arguments object / parameter setup for the root frame +, undefined, {}); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--173.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--173.js new file mode 100644 index 00000000..212379bf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--173.js @@ -0,0 +1,966 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 13, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(fake_17, fake_15, globals, fake_16, bytecode, constants) { + this.z = bytecode; + this.r = constants; + this.K = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.e = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.G = fake_16; + this.A = []; + this.w = fake_15; + this.c = fake_17; + this.n = 1; + this.q = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) + +// Closure & Frame +function Closure(fake_14, fn) { + this.ad = fn; + this.C = []; + this.m = fake_14; + this.prototype = {}; // <- default prototype object for `new` +} +function Upvalue(absSlot, regs, fake_1) { + this.H = fake_1; + this.A = regs; // shared reference to VM._regs flat array + this.p = absSlot; // absolute index; stable as long as frame is alive + this.a = false; + this.ab = undefined; +} +Upvalue.prototype.y = function (fake_6, fake_7, fake_9, v, fake_8) { + if (this.a) this.ab = v;else this.A[this.p] = v; + this.W = [fake_6, fake_9]; + this.D = fake_7 && fake_8; +}; +VM.prototype.I = function (fake_18, fake_20, fake_19, fake_21) { + this.J = fake_21; + this.g = fake_20; + this.f = fake_18; + return this.z[this.A[this.q + 0]++]; + this.ae = fake_19; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). + +VM.prototype.R = function (fake_33, fake_34, fake_32, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.e; + this.X = fake_33; + this.u = fake_34; + if (!uvs) return; // nothing in this program ever captured a local + this.ag = fake_32; + var regs = this.A; + var hi = fp + regs[fp + 6]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.i("q", "z", {}, "q"); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.E = function (fake_3, fake_4, fake_2, fake_5) { + this.o = fake_3 || fake_5; + this.al = fake_2; + return this.a ? this.ab : this.A[this.p]; + this.O = fake_4; +}; +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.l = function (keyIn, idxIn, fake_30, fake_31) { + this.am = fake_30; + var idx = idxIn ?? this.I({}, {}, [], "z"); + var key = keyIn ?? this.I(undefined, [], null, []); + var v = this.r[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + this.S = fake_31; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.h = function (thisVal, fake_22, fake_23, fake_24, closure, fake_25, args, retDst) { + var fn = closure.ad; + var regs = this.A; + var fp = this.n; + var size = 8 + fn.U; + var end = fp + size; + this.x = fake_24; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + this.Q = [fake_25, fake_22]; + this.V = fake_23; + var base = fp + 8; + regs[fp + 0] = fn.j; + regs[fp + 1] = this.q; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.k; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.j; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.n = end; + if (args) { + if (fn.ak) { + var restSlot = fn.k - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.U; a++) regs[base + a] = args[a]; + } + if (fn.k < fn.U) regs[base + fn.k] = args; + } + this.q = fp; +}; +VM.prototype.t = function (fake_27, fake_26, fake_29, slot, fake_28, fp) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.A[fp + 7] + slot; + this.ah = fake_27 || fake_26; + this.F = fake_28 && fake_29; + var uvs = this.e || (this.e = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, this.A, [])); +}; +Upvalue.prototype.i = function (fake_13, fake_12, fake_11, fake_10) { + this.ab = this.A[this.p]; + this.B = [fake_13, fake_12]; + this.a = true; + this.Y = fake_11; + this.L = fake_10; +}; +VM.prototype.P = function (thisVal, fake_37, fake_35, fake_38, closure, fake_36, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.h(thisVal, [], null, undefined, closure, "x", args, 0); + this.aa = fake_37; + this.d = fake_38; + while (true) { + var fp = this.q; + var regs = this.A; + var bc = this.z; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.z[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.z.length; i++) this.z[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.n; i2++) this.A[i2] = undefined; + op = 39; + regs[fp + 0] = this.z.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 0: + { + var dst = this.I({}, [], "x", 66); + regs[base + dst] = this.l(); + break; + } + case 1: + { + var dst = this.I(4, {}, "y", "k"); + regs[base + dst] = this.I([], null, {}, 64); + break; + } + case 2: + { + var dst = this.I("x", 5, undefined, 91); + var globalName = this.l(); + if (!(globalName in this.K)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.K[globalName]; + break; + } + case 3: + { + var dst = this.I("y", null, undefined, {}); + regs[base + dst] = regs[fp + 4].C[this.I("y", [], undefined, undefined)].E(78, [], undefined, []); + break; + } + case 4: + { + var dst = this.I([], {}, "k", "y"); + regs[base + dst] = regs[fp + 3]; + break; + } + case 5: + { + var dst = this.I([], undefined, [], "k"); + regs[base + dst] = regs[base + this.I(43, null, 67, [])]; + break; + } + case 6: + { + // globals[globalName] = regs[src] + this.K[this.l()] = regs[base + this.I([], undefined, 65, null)]; + break; + } + case 7: + { + var uvIdx = this.I({}, null, undefined, []); + regs[fp + 4].C[uvIdx].y([], {}, [], regs[base + this.I("y", undefined, 89, 54)], {}); + break; + } + case 8: + { + // dst = regs[obj][regs[key]] + var dst = this.I(null, {}, [], "x"); + var obj = regs[base + this.I(29, [], 40, null)]; + var key = regs[base + this.I(8, "y", null, {})]; + regs[base + dst] = obj[key]; + break; + } + case 9: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.I(undefined, undefined, {}, 35)]; + var key = regs[base + this.I({}, 55, null, [])]; + var val = regs[base + this.I([], [], 59, 84)]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 10: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.I({}, {}, "k", "k"); + var obj = regs[base + this.I(null, "z", [], undefined)]; + var key = regs[base + this.I([], null, null, undefined)]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 11: + { + var dst = this.I(null, undefined, 13, 74); + var a = regs[base + this.I({}, null, "z", null)]; + regs[base + dst] = a + regs[base + this.I(null, null, null, 8)]; + break; + } + case 12: + { + var dst = this.I(4, null, {}, undefined); + var a = regs[base + this.I([], null, {}, [])]; + regs[base + dst] = a - regs[base + this.I("k", [], [], undefined)]; + break; + } + case 13: + { + var dst = this.I(null, undefined, [], undefined); + var a = regs[base + this.I(undefined, {}, 96, "q")]; + regs[base + dst] = a * regs[base + this.I(undefined, undefined, [], {})]; + break; + } + case 14: + { + var dst = this.I(undefined, {}, "q", []); + var a = regs[base + this.I(74, 52, {}, 63)]; + regs[base + dst] = a / regs[base + this.I(undefined, "z", [], [])]; + break; + } + case 15: + { + var dst = this.I([], undefined, [], "k"); + var a = regs[base + this.I(34, 11, 14, [])]; + regs[base + dst] = a % regs[base + this.I([], "y", "k", undefined)]; + break; + } + case 60: + { + // Math.pow instead of `**` + var dst = this.I("y", {}, [], undefined); + var a = regs[base + this.I({}, 80, {}, {})]; + regs[base + dst] = Math.pow(a, regs[base + this.I(undefined, 78, {}, {})]); + break; + } + case 16: + { + var dst = this.I(null, {}, 41, {}); + var a = regs[base + this.I(null, [], 45, 54)]; + regs[base + dst] = a & regs[base + this.I(null, 92, null, "x")]; + break; + } + case 17: + { + var dst = this.I(null, undefined, null, null); + var a = regs[base + this.I(undefined, [], "k", [])]; + regs[base + dst] = a | regs[base + this.I([], [], 50, null)]; + break; + } + case 18: + { + var dst = this.I(44, [], null, {}); + var a = regs[base + this.I("x", undefined, null, null)]; + regs[base + dst] = a ^ regs[base + this.I(0, null, "q", {})]; + break; + } + case 19: + { + var dst = this.I(null, "k", null, {}); + var a = regs[base + this.I("k", undefined, "x", {})]; + regs[base + dst] = a << regs[base + this.I(null, 79, null, [])]; + break; + } + case 20: + { + var dst = this.I({}, null, undefined, undefined); + var a = regs[base + this.I({}, "z", null, 82)]; + regs[base + dst] = a >> regs[base + this.I(null, 72, "q", null)]; + break; + } + case 21: + { + var dst = this.I(62, null, undefined, null); + var a = regs[base + this.I([], undefined, null, "q")]; + regs[base + dst] = a >>> regs[base + this.I(null, "z", null, null)]; + break; + } + + // Comparison (dst, src1, src2) + case 22: + { + var dst = this.I("k", {}, [], 22); + var a = regs[base + this.I(null, undefined, undefined, 48)]; + regs[base + dst] = a < regs[base + this.I({}, "x", null, "z")]; + break; + } + case 23: + { + var dst = this.I(28, "k", {}, {}); + var a = regs[base + this.I(88, [], undefined, [])]; + regs[base + dst] = a > regs[base + this.I("z", "y", [], undefined)]; + break; + } + case 24: + { + var dst = this.I([], undefined, [], "x"); + var a = regs[base + this.I(89, [], "k", [])]; + regs[base + dst] = a <= regs[base + this.I([], "x", null, null)]; + break; + } + case 25: + { + var dst = this.I({}, [], {}, []); + var a = regs[base + this.I(12, "q", null, {})]; + regs[base + dst] = a >= regs[base + this.I([], null, {}, null)]; + break; + } + case 26: + { + var dst = this.I(undefined, null, undefined, "y"); + var a = regs[base + this.I([], [], undefined, {})]; + regs[base + dst] = a === regs[base + this.I([], {}, {}, "z")]; + break; + } + case 27: + { + var dst = this.I({}, null, "x", undefined); + var a = regs[base + this.I([], undefined, null, "y")]; + regs[base + dst] = a !== regs[base + this.I(14, "z", [], [])]; + break; + } + case 28: + { + var dst = this.I("z", null, 66, []); + var a = regs[base + this.I({}, null, 88, 0)]; + regs[base + dst] = a == regs[base + this.I([], undefined, undefined, 67)]; + break; + } + case 29: + { + var dst = this.I([], "q", null, "z"); + var a = regs[base + this.I(null, null, undefined, "k")]; + regs[base + dst] = a != regs[base + this.I(null, {}, null, [])]; + break; + } + case 30: + { + var dst = this.I(undefined, [], {}, null); + var a = regs[base + this.I(null, 97, undefined, 17)]; + regs[base + dst] = a in regs[base + this.I({}, undefined, 11, undefined)]; + break; + } + case 31: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.I([], {}, "y", "k"); + var obj = regs[base + this.I([], "k", "k", [])]; + regs[base + dst] = obj instanceof regs[base + this.I("k", 69, null, 29)]; + break; + } + + // Unary (dst, src) + case 32: + { + var dst = this.I("z", undefined, undefined, undefined); + regs[base + dst] = -regs[base + this.I(39, {}, 96, null)]; + break; + } + case 33: + { + var dst = this.I([], [], 31, "x"); + regs[base + dst] = +regs[base + this.I({}, "y", "q", null)]; + break; + } + case 34: + { + var dst = this.I("y", "q", 59, null); + regs[base + dst] = !regs[base + this.I(68, undefined, 75, [])]; + break; + } + case 35: + { + var dst = this.I(null, undefined, [], "x"); + regs[base + dst] = ~regs[base + this.I(71, "z", [], 1)]; + break; + } + case 36: + { + var dst = this.I(null, {}, undefined, null); + regs[base + dst] = typeof regs[base + this.I({}, null, [], 44)]; + break; + } + case 37: + { + var dst = this.I("y", "q", {}, "y"); + this.I([], [], 42, "z"); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 38: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.I(undefined, [], null, "x"); + var name = this.l(); + var val = Object.prototype.hasOwnProperty.call(this.K, name) ? this.K[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39: + regs[fp + 0] = this.I(40, undefined, 39, 28); + break; + case 40: + { + var src = this.I(undefined, {}, [], undefined); + var target = this.I(undefined, {}, undefined, null); + if (!regs[base + src]) regs[fp + 0] = target; + break; + } + case 41: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.I([], undefined, {}, null); + var target = this.I(null, "k", 45, 6); + if (regs[base + src]) regs[fp + 0] = target; + break; + } + + // Calls + case 42: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.I(undefined, {}, 26, undefined); + var callee = regs[base + this.I(null, undefined, "z", [])]; + var argc = this.I({}, undefined, undefined, {}); + var args; + if (argc === 65535) { + args = regs[base + this.I([], 46, null, "q")]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.I("q", [], null, [])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.h(this.K, [], 1, [], closure, 78, args, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 43: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.I("k", 76, 31, undefined); + var receiver = regs[base + this.I("x", {}, undefined, {})]; + var callee = regs[base + this.I(39, undefined, 83, 76)]; + var argc = this.I(53, "y", undefined, "k"); + var args; + if (argc === 65535) { + args = regs[base + this.I(44, null, null, {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.I({}, [], {}, null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.h(receiver, "x", 81, undefined, closure, [], args, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 44: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.I([], 86, {}, "k"); + var callee = regs[base + this.I("y", [], "x", null)]; + var argc = this.I("z", [], [], null); + var args; + if (argc === 65535) { + args = regs[base + this.I("k", 32, [], 27)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.I(undefined, 95, null, 7)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.h(Object.create(closure.prototype || null), {}, undefined, "z", closure, [], args, dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 45: + { + var retVal = regs[base + this.I(undefined, [], [], null)]; + this.R({}, [], {}, fp); // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.n = fp; + this.q = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 46: + throw regs[base + this.I("q", null, [], "k")]; + + // Closures + case 47: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.I({}, "y", [], "q"); + var startPc = this.I([], undefined, [], []); + var paramCount = this.I(17, 85, null, {}); + var regCount = this.I([], null, 15, 36); + var uvCount = this.I([], [], undefined, undefined); + var hasRest = this.I(null, 93, null, undefined); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.I("z", {}, null, undefined); + var uvIndex = this.I({}, "k", {}, null); + uvDescs[i] = { + s: isLocalRaw, + N: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + k: paramCount, + U: regCount, + j: startPc, + ak: hasRest + }; + var closure = new Closure(73, fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.s) { + closure.C.push(this.t("y", 42, 61, uvd.N, undefined, fp)); + } else { + closure.C.push(regs[fp + 4].C[uvd.N]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM("q", null, self.K, undefined, self.z, self.r).P(this == null ? self.K : this, 6, null, {}, c, {}, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 48: + { + // dst, count, [elemReg...] + var dst = this.I(91, [], undefined, undefined); + var count = this.I({}, "k", [], undefined); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.I(3, "k", [], [])]; + regs[base + dst] = elems; + break; + } + case 49: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.I(undefined, null, {}, undefined); + var pairCount = this.I([], "k", [], []); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.I({}, "q", undefined, "y")]; + var val = regs[base + this.I(undefined, {}, "q", [])]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50: + { + // obj, key, fn + var obj = regs[base + this.I([], "k", [], null)]; + var key = regs[base + this.I(null, 59, [], 65)]; + var getterFn = regs[base + this.I([], "y", undefined, "y")]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 51: + { + // obj, key, fn + var obj = regs[base + this.I([], "k", undefined, undefined)]; + var key = regs[base + this.I(null, null, {}, 42)]; + var setterFn = regs[base + this.I({}, [], 59, undefined)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.I("x", 55, null, 50); + var obj = regs[base + this.I(69, undefined, 83, 55)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + T: keys, + ac: 0 + }; + break; + } + case 53: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.I([], {}, null, "z"); + var iter = regs[base + this.I({}, undefined, undefined, [])]; + var exitTarget = this.I(null, 32, undefined, {}); + if (iter.ac >= iter.T.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.T[iter.ac++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 54: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + af: this.I([], {}, "y", "y"), + b: this.I(undefined, 8, "k", null) + }); + break; + } + case 55: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 59: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + M: this.I(undefined, {}, "k", null), + Z: this.I(undefined, {}, 95, null), + v: this.I("z", 50, [], undefined), + aj: this.I(undefined, null, 81, []) + }); + break; + } + + // Self-modifying bytecode + case 56: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.I(undefined, "y", {}, "x"); + var sliceStart = this.I({}, [], null, 46); + var sliceEnd = this.I(undefined, {}, {}, "q"); + var pk = this.I({}, undefined, "x", []) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.z[destPc + (pi - sliceStart)] = (this.z[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 58: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.I(undefined, {}, 58, 43)]; + break; + } + case 57: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 0] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.q; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.R([], [], null, searchFrame); + this.n = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.q = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.b !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.b] = err; + regs[handledFrame + 0] = h.af; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.Z] = h.aj; + regs[hBase + h.v] = err; + regs[handledFrame + 0] = h.M; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.n = handledFrame + regs[handledFrame + 6]; + this.q = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.q) return this.A[0]; + } + this.ai = fake_36 || fake_35; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM({}, {}, globals, "z", decodeBytecode(BYTECODE), CONSTANTS); +vm.P(undefined, 38, 34, undefined, new Closure(null, { + k: 0, + U: 7, + j: 0 +}), "y", null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--174.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--174.js new file mode 100644 index 00000000..a024ed38 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--classObfuscation--174.js @@ -0,0 +1,946 @@ +function VM(constants, bytecode, fake_13, fake_15, fake_14, fake_12, globals) { + this.G = bytecode; + this.w = constants; + this.Z = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.E = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.r = fake_14; + this.O = []; + this.z = fake_12 && fake_15; + this.M = fake_13; + this.L = 1; + this.U = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +// Closure & Frame +function Closure(fake_10, fake_11, fn) { + this.g = [fake_10, fake_11]; + this.l = fn; + this.D = []; + this.prototype = {}; // <- default prototype object for `new` +} +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(fake_1, fake_2, regs, absSlot, fake_3) { + this.O = regs; // shared reference to VM._regs flat array + this.u = fake_2 || fake_3; + this.j = fake_1; + this.q = absSlot; // absolute index; stable as long as frame is alive + this.b = false; + this.a = undefined; +} +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CLOSURE_MAP = new WeakMap(); +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 13, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +Upvalue.prototype.V = function (fake_5, fake_4, fake_6) { + return this.b ? this.a : this.O[this.q]; + this.C = fake_6; + this.Y = fake_4; + this.i = fake_5; +}; +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.c = function (closure, fake_20, retDst, args, fake_19, thisVal, fake_18) { + var fn = closure.l; + var regs = this.O; + var fp = this.L; + var size = 8 + fn.n; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 8; + regs[fp + 0] = fn.aa; + this.X = fake_20; + regs[fp + 1] = this.U; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 4] = closure; + regs[fp + 6] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.h; + this.W = fake_19; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.aa; + this.B = fake_18; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.L = end; + if (args) { + if (fn.R) { + var restSlot = fn.h - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.n; a++) regs[base + a] = args[a]; + } + if (fn.h < fn.n) regs[base + fn.h] = args; + } + this.U = fp; +}; +VM.prototype.ab = function (fake_25, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.E; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this.O; + var hi = fp + regs[fp + 6]; + this.v = fake_25; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.m(null, {}); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.J = function (fp, fake_22, fake_21, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.O[fp + 7] + slot; + var uvs = this.E || (this.E = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(undefined, null, this.O, absSlot, [])); + this.H = [fake_22, fake_21]; +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.k = function (idxIn, fake_24, fake_23, keyIn) { + var idx = idxIn ?? this.o(undefined, 53); + var key = keyIn ?? this.o(null, "y"); + var v = this.w[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + this.d = [fake_24, fake_23]; + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +Upvalue.prototype.F = function (fake_7, v) { + this.K = fake_7; + if (this.b) this.a = v;else this.O[this.q] = v; +}; +Upvalue.prototype.m = function (fake_9, fake_8) { + this.a = this.O[this.q]; + this.b = true; + this.A = [fake_8, fake_9]; +}; +VM.prototype.o = function (fake_17, fake_16) { + this.T = [fake_16, fake_17]; + return this.G[this.O[this.U + 0]++]; +}; +VM.prototype.t = function (thisVal, fake_26, args, fake_28, closure, fake_27) { + var now = () => { + return performance.now(); + }; + this.f = fake_26; + var lastTime = now(); + this.c(closure, undefined, 0, args, "x", thisVal, undefined); + while (true) { + var fp = this.U; + var regs = this.O; + var bc = this.G; + var pc = regs[fp + 0]; + if (pc >= bc.length) break; + regs[fp + 0] = pc + 1; + var op = this.G[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.G.length; i++) this.G[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.L; i2++) this.O[i2] = undefined; + op = 39; + regs[fp + 0] = this.G.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 0: + { + var dst = this.o([], {}); + regs[base + dst] = this.k(); + break; + } + case 1: + { + var dst = this.o([], 12); + regs[base + dst] = this.o("y", null); + break; + } + case 2: + { + var dst = this.o(undefined, undefined); + var globalName = this.k(); + if (!(globalName in this.Z)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.Z[globalName]; + break; + } + case 3: + { + var dst = this.o(undefined, undefined); + regs[base + dst] = regs[fp + 4].D[this.o([], 81)].V("q", "y", 9); + break; + } + case 4: + { + var dst = this.o([], undefined); + regs[base + dst] = regs[fp + 3]; + break; + } + case 5: + { + var dst = this.o("k", []); + regs[base + dst] = regs[base + this.o(82, [])]; + break; + } + case 6: + { + // globals[globalName] = regs[src] + this.Z[this.k()] = regs[base + this.o({}, "z")]; + break; + } + case 7: + { + var uvIdx = this.o(undefined, undefined); + regs[fp + 4].D[uvIdx].F(57, regs[base + this.o(44, undefined)]); + break; + } + case 8: + { + // dst = regs[obj][regs[key]] + var dst = this.o({}, null); + var obj = regs[base + this.o(undefined, undefined)]; + var key = regs[base + this.o([], 64)]; + regs[base + dst] = obj[key]; + break; + } + case 9: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.o({}, null)]; + var key = regs[base + this.o("z", undefined)]; + var val = regs[base + this.o(null, {})]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 10: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.o(85, undefined); + var obj = regs[base + this.o(undefined, [])]; + var key = regs[base + this.o("q", 64)]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 11: + { + var dst = this.o("z", []); + var a = regs[base + this.o(70, [])]; + regs[base + dst] = a + regs[base + this.o([], 95)]; + break; + } + case 12: + { + var dst = this.o(null, {}); + var a = regs[base + this.o(null, 88)]; + regs[base + dst] = a - regs[base + this.o([], {})]; + break; + } + case 13: + { + var dst = this.o("x", []); + var a = regs[base + this.o("k", {})]; + regs[base + dst] = a * regs[base + this.o(undefined, null)]; + break; + } + case 14: + { + var dst = this.o(null, undefined); + var a = regs[base + this.o("z", 3)]; + regs[base + dst] = a / regs[base + this.o("k", 77)]; + break; + } + case 15: + { + var dst = this.o([], undefined); + var a = regs[base + this.o(null, [])]; + regs[base + dst] = a % regs[base + this.o(null, "k")]; + break; + } + case 60: + { + // Math.pow instead of `**` + var dst = this.o(undefined, 37); + var a = regs[base + this.o("y", {})]; + regs[base + dst] = Math.pow(a, regs[base + this.o([], "x")]); + break; + } + case 16: + { + var dst = this.o([], "x"); + var a = regs[base + this.o({}, "z")]; + regs[base + dst] = a & regs[base + this.o("y", 15)]; + break; + } + case 17: + { + var dst = this.o(null, 51); + var a = regs[base + this.o([], [])]; + regs[base + dst] = a | regs[base + this.o(null, undefined)]; + break; + } + case 18: + { + var dst = this.o([], {}); + var a = regs[base + this.o({}, {})]; + regs[base + dst] = a ^ regs[base + this.o({}, [])]; + break; + } + case 19: + { + var dst = this.o(32, null); + var a = regs[base + this.o(56, [])]; + regs[base + dst] = a << regs[base + this.o(87, {})]; + break; + } + case 20: + { + var dst = this.o([], null); + var a = regs[base + this.o(74, [])]; + regs[base + dst] = a >> regs[base + this.o({}, "y")]; + break; + } + case 21: + { + var dst = this.o([], []); + var a = regs[base + this.o({}, null)]; + regs[base + dst] = a >>> regs[base + this.o(undefined, 50)]; + break; + } + + // Comparison (dst, src1, src2) + case 22: + { + var dst = this.o(null, []); + var a = regs[base + this.o(undefined, 3)]; + regs[base + dst] = a < regs[base + this.o(undefined, "k")]; + break; + } + case 23: + { + var dst = this.o(59, null); + var a = regs[base + this.o("y", 48)]; + regs[base + dst] = a > regs[base + this.o([], {})]; + break; + } + case 24: + { + var dst = this.o(null, 17); + var a = regs[base + this.o([], 98)]; + regs[base + dst] = a <= regs[base + this.o("z", "k")]; + break; + } + case 25: + { + var dst = this.o(undefined, 82); + var a = regs[base + this.o([], [])]; + regs[base + dst] = a >= regs[base + this.o(undefined, undefined)]; + break; + } + case 26: + { + var dst = this.o(null, {}); + var a = regs[base + this.o(undefined, "k")]; + regs[base + dst] = a === regs[base + this.o(74, [])]; + break; + } + case 27: + { + var dst = this.o(undefined, "k"); + var a = regs[base + this.o(null, {})]; + regs[base + dst] = a !== regs[base + this.o(3, null)]; + break; + } + case 28: + { + var dst = this.o({}, []); + var a = regs[base + this.o(29, undefined)]; + regs[base + dst] = a == regs[base + this.o({}, "x")]; + break; + } + case 29: + { + var dst = this.o({}, []); + var a = regs[base + this.o(null, "z")]; + regs[base + dst] = a != regs[base + this.o([], undefined)]; + break; + } + case 30: + { + var dst = this.o("k", "k"); + var a = regs[base + this.o([], 66)]; + regs[base + dst] = a in regs[base + this.o(null, null)]; + break; + } + case 31: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.o(undefined, null); + var obj = regs[base + this.o("q", null)]; + regs[base + dst] = obj instanceof regs[base + this.o(undefined, [])]; + break; + } + + // Unary (dst, src) + case 32: + { + var dst = this.o([], 19); + regs[base + dst] = -regs[base + this.o(undefined, 30)]; + break; + } + case 33: + { + var dst = this.o("x", {}); + regs[base + dst] = +regs[base + this.o(60, 97)]; + break; + } + case 34: + { + var dst = this.o([], null); + regs[base + dst] = !regs[base + this.o(null, {})]; + break; + } + case 35: + { + var dst = this.o(6, null); + regs[base + dst] = ~regs[base + this.o(null, undefined)]; + break; + } + case 36: + { + var dst = this.o("y", 22); + regs[base + dst] = typeof regs[base + this.o({}, 11)]; + break; + } + case 37: + { + var dst = this.o({}, null); + this.o("q", "x"); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 38: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.o(undefined, "q"); + var name = this.k(); + var val = Object.prototype.hasOwnProperty.call(this.Z, name) ? this.Z[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39: + regs[fp + 0] = this.o("z", "z"); + break; + case 40: + { + var src = this.o("y", undefined); + var target = this.o([], undefined); + if (!regs[base + src]) regs[fp + 0] = target; + break; + } + case 41: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.o("k", "q"); + var target = this.o(53, []); + if (regs[base + src]) regs[fp + 0] = target; + break; + } + + // Calls + case 42: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.o([], null); + var callee = regs[base + this.o({}, {})]; + var argc = this.o(undefined, []); + var args; + if (argc === 65535) { + args = regs[base + this.o(null, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.o(null, undefined)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.c(closure, "x", dst << 1, args, [], this.Z, null); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 43: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.o(null, []); + var receiver = regs[base + this.o({}, undefined)]; + var callee = regs[base + this.o(undefined, null)]; + var argc = this.o(null, null); + var args; + if (argc === 65535) { + args = regs[base + this.o({}, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.o(null, "z")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.c(closure, 57, dst << 1, args, [], receiver, {}); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 44: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.o([], "y"); + var callee = regs[base + this.o([], "y")]; + var argc = this.o(null, "z"); + var args; + if (argc === 65535) { + args = regs[base + this.o(20, [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.o([], null)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.c(closure, 30, dst << 1 | 1, args, "q", Object.create(closure.prototype || null), []); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 45: + { + var retVal = regs[base + this.o({}, 13)]; + this.ab(null, fp); // must happen before frame is abandoned + + var caller = regs[fp + 1]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 6]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.L = fp; + this.U = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 46: + throw regs[base + this.o("y", null)]; + + // Closures + case 47: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.o("q", undefined); + var startPc = this.o(undefined, []); + var paramCount = this.o("z", null); + var regCount = this.o([], []); + var uvCount = this.o(undefined, {}); + var hasRest = this.o(89, null); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.o([], []); + var uvIndex = this.o({}, []); + uvDescs[i] = { + I: isLocalRaw, + Q: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + h: paramCount, + n: regCount, + aa: startPc, + R: hasRest + }; + var closure = new Closure(45, undefined, fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.I) { + closure.D.push(this.J(fp, 79, 73, uvd.Q)); + } else { + closure.D.push(regs[fp + 4].D[uvd.Q]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.w, self.G, 2, null, null, undefined, self.Z).t(this == null ? self.Z : this, 36, Array.prototype.slice.call(arguments), 66, c, 70); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 48: + { + // dst, count, [elemReg...] + var dst = this.o({}, 67); + var count = this.o(undefined, "x"); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.o(null, 63)]; + regs[base + dst] = elems; + break; + } + case 49: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.o(91, undefined); + var pairCount = this.o({}, null); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.o([], 79)]; + var val = regs[base + this.o("k", "x")]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50: + { + // obj, key, fn + var obj = regs[base + this.o(null, "q")]; + var key = regs[base + this.o(undefined, undefined)]; + var getterFn = regs[base + this.o([], null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 51: + { + // obj, key, fn + var obj = regs[base + this.o(null, null)]; + var key = regs[base + this.o(undefined, {})]; + var setterFn = regs[base + this.o(93, null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.o(null, undefined); + var obj = regs[base + this.o(null, null)]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + x: keys, + N: 0 + }; + break; + } + case 53: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.o({}, undefined); + var iter = regs[base + this.o({}, undefined)]; + var exitTarget = this.o({}, 25); + if (iter.N >= iter.x.length) { + regs[fp + 0] = exitTarget; + } else { + regs[base + dst] = iter.x[iter.N++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 54: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + e: this.o(undefined, undefined), + ac: this.o(null, 44) + }); + break; + } + case 55: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 59: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + P: this.o(undefined, "q"), + y: this.o(null, "y"), + s: this.o(undefined, null), + p: this.o(null, "z") + }); + break; + } + + // Self-modifying bytecode + case 56: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.o("q", {}); + var sliceStart = this.o({}, 5); + var sliceEnd = this.o(26, null); + var pk = this.o({}, []) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.G[destPc + (pi - sliceStart)] = (this.G[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 58: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 0] = regs[base + this.o(undefined, undefined)]; + break; + } + case 57: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 0] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.U; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.ab(undefined, searchFrame); + this.L = searchFrame; + searchFrame = regs[searchFrame + 1]; + this.U = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 7]; + if (h.ac !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.ac] = err; + regs[handledFrame + 0] = h.e; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.y] = h.p; + regs[hBase + h.s] = err; + regs[handledFrame + 0] = h.P; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.L = handledFrame + regs[handledFrame + 6]; + this.U = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.U) return this.O[0]; + } + this.S = fake_28 || fake_27; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(CONSTANTS, decodeBytecode(BYTECODE), "y", [], {}, [], globals); +vm.t(undefined, undefined, null // no arguments object / parameter setup for the root frame +, undefined, new Closure({}, "k", { + h: 0, + n: 7, + aa: 0 +}), null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--172.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--172.js new file mode 100644 index 00000000..bdfe49e8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--172.js @@ -0,0 +1,15 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0);T(new C([4,1,2,2,0,0,0,3,1,0,0,4,2,0,0,5,3,0,13,6,4,5,0,4,4,0,11,5,6,4,0,4,5,0,15,6,5,4,9,2,3,6,0,2,6,0,45,2],["window","TEST_OUTPUT",6,7,3,5,void 0],U),new B({m:0,v:7,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--173.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--173.js new file mode 100644 index 00000000..bdfe49e8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--173.js @@ -0,0 +1,15 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0);T(new C([4,1,2,2,0,0,0,3,1,0,0,4,2,0,0,5,3,0,13,6,4,5,0,4,4,0,11,5,6,4,0,4,5,0,15,6,5,4,9,2,3,6,0,2,6,0,45,2],["window","TEST_OUTPUT",6,7,3,5,void 0],U),new B({m:0,v:7,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--174.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--174.js new file mode 100644 index 00000000..bdfe49e8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minify--174.js @@ -0,0 +1,15 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0);T(new C([4,1,2,2,0,0,0,3,1,0,0,4,2,0,0,5,3,0,13,6,4,5,0,4,4,0,11,5,6,4,0,4,5,0,15,6,5,4,9,2,3,6,0,2,6,0,45,2],["window","TEST_OUTPUT",6,7,3,5,void 0],U),new B({m:0,v:7,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--172.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--172.js new file mode 100644 index 00000000..3aad86b7 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--172.js @@ -0,0 +1,16 @@ +function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var c=new WeakMap;function A(a,g){this.g=g;this.e=a;this.o=!1;this.u=void 0}function B(a,g,k){this.q=k;this.v=a;this.h=g;this.m=null;this.g=[];this.s=1;this.n=0}function C(a,g,k){this.t=k;this.i=[];this.E=[g,a];this.prototype={}}function fa(a,g,k){g=a.g[g+7]+k;k=a.m||(a.m=[]);return k[g]||(k[g]=new A(g,a.g))} +B.prototype.d=function(a,g,k,b,h,f){a=g.t;h=this.g;for(var m=this.s,d=8+a.l,e=m+d;h.length>>13)&65535);return g}function Q(a,g,k,b){var h=a.m;if(h){var f=a.g;a.b=k||b;a=g+f[g+6];for(g=f[g+7];g=a.q.length)break;b[f+0]=m+1;m=a.q[m];try{var d=b[f+7];switch(m){case 0:var e=J(a);b[d+e]=D(a);break;case 1:e=J(a);b[d+e]=J(a);break;case 2:e=J(a);var p=D(a);if(!(p in a.h))throw new ReferenceError(`${p} is not defined`);b[d+e]=a.h[p];break;case 3:e=J(a);b[d+e]=b[f+4].i[J(a)].w(void 0);break;case 4:e=J(a);b[d+e]=b[f+3];break;case 5:e=J(a);b[d+e]=b[d+J(a)];break;case 6:a.h[D(a)]=b[d+J(a)];break;case 7:var ha=J(a); +b[f+4].i[ha].c(void 0,b[d+J(a)],null,"z");break;case 8:e=J(a);var n=b[d+J(a)],q=b[d+J(a)];b[d+e]=n[q];break;case 9:n=b[d+J(a)];q=b[d+J(a)];var x=b[d+J(a)];Reflect.set(n,q,x);break;case 10:e=J(a);n=b[d+J(a)];q=b[d+J(a)];b[d+e]=delete n[q];break;case 11:e=J(a);var l=b[d+J(a)];b[d+e]=l+b[d+J(a)];break;case 12:e=J(a);l=b[d+J(a)];b[d+e]=l-b[d+J(a)];break;case 13:e=J(a);l=b[d+J(a)];b[d+e]=l*b[d+J(a)];break;case 14:e=J(a);l=b[d+J(a)];b[d+e]=l/b[d+J(a)];break;case 15:e=J(a);l=b[d+J(a)];b[d+e]=l%b[d+J(a)]; +break;case 60:e=J(a);l=b[d+J(a)];b[d+e]=Math.pow(l,b[d+J(a)]);break;case 16:e=J(a);l=b[d+J(a)];b[d+e]=l&b[d+J(a)];break;case 17:e=J(a);l=b[d+J(a)];b[d+e]=l|b[d+J(a)];break;case 18:e=J(a);l=b[d+J(a)];b[d+e]=l^b[d+J(a)];break;case 19:e=J(a);l=b[d+J(a)];b[d+e]=l<>b[d+J(a)];break;case 21:e=J(a);l=b[d+J(a)];b[d+e]=l>>>b[d+J(a)];break;case 22:e=J(a);l=b[d+J(a)];b[d+e]=lb[d+J(a)];break;case 24:e=J(a); +l=b[d+J(a)];b[d+e]=l<=b[d+J(a)];break;case 25:e=J(a);l=b[d+J(a)];b[d+e]=l>=b[d+J(a)];break;case 26:e=J(a);l=b[d+J(a)];b[d+e]=l===b[d+J(a)];break;case 27:e=J(a);l=b[d+J(a)];b[d+e]=l!==b[d+J(a)];break;case 28:e=J(a);l=b[d+J(a)];b[d+e]=l==b[d+J(a)];break;case 29:e=J(a);l=b[d+J(a)];b[d+e]=l!=b[d+J(a)];break;case 30:e=J(a);l=b[d+J(a)];b[d+e]=l in b[d+J(a)];break;case 31:e=J(a);n=b[d+J(a)];b[d+e]=n instanceof b[d+J(a)];break;case 32:e=J(a);b[d+e]=-b[d+J(a)];break;case 33:e=J(a);b[d+e]=+b[d+J(a)];break; +case 34:e=J(a);b[d+e]=!b[d+J(a)];break;case 35:e=J(a);b[d+e]=~b[d+J(a)];break;case 36:e=J(a);b[d+e]=typeof b[d+J(a)];break;case 37:e=J(a);J(a);b[d+e]=void 0;break;case 38:e=J(a);var T=D(a);x=Object.prototype.hasOwnProperty.call(a.h,T)?a.h[T]:void 0;b[d+e]=typeof x;break;case 39:b[f+0]=J(a);break;case 40:var K=J(a),L=J(a);b[d+K]||(b[f+0]=L);break;case 41:K=J(a);L=J(a);b[d+K]&&(b[f+0]=L);break;case 42:e=J(a);var r=b[d+J(a)],t=J(a);if(t===65535)k=b[d+J(a)];else for(k=Array(t),h=0;h>1):0]=E;break;case 46:throw b[d+J(a)];case 47:e=J(a);var ja=J(a),ka=J(a),la=J(a),W=J(a),ma=J(a),N=Array(W);for(h=0;h=H.x.length?b[f+0]=sa:b[d+e]=H.x[H.B++];break;case 54:var v=b[f+5];v||(b[f+5]=v=[]);v.push({F:J(a),A:J(a)});break;case 55:b[f+5].pop();break;case 59:(v=b[f+5])||(b[f+5]=v=[]);v.push({k:J(a),C:J(a),f:J(a),D:J(a)});break;case 56:var ca=J(a),da=J(a),ta=J(a),I=J(a)^ca|0;for(f=da;f>>13)>>>0;break;case 58:b[f+0]=b[d+J(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+ +" at pc "+(b[f+0]-1));}}catch(y){f=0;for(m=a.n;m;){var w=b[m+5];if(w&&w.length>0){f=m;break}Q(a,m,[],{});a.s=m;m=b[m+1];a.n=m}if(!f)throw y;m=b[f+5].pop();w=b[f+7];m.A!==void 0?(b[w+m.A]=y,b[f+0]=m.F):(b[w+m.C]=m.D,b[w+m.f]=y,b[f+0]=m.k);a.s=f+b[f+6];a.n=f}if(!a.n)return a.g[0]}}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new B(["window","TEST_OUTPUT",6,7,3,5,void 0],S,[4,1,2,2,0,0,0,3,1,0,0,4,2,0,0,5,3,0,13,6,4,5,0,4,4,0,11,5,6,4,0,4,5,0,15,6,5,4,9,2,3,6,0,2,6,0,45,2]),new C(35,void 0,{j:0,l:7,p:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--173.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--173.js new file mode 100644 index 00000000..81ad14ba --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--173.js @@ -0,0 +1,16 @@ +function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}function c(a,b,f,h,m){this.z=h;this.r=m;this.h=f;this.e=null;this.g=[];this.w=b;this.c=a;this.n=1;this.q=0}var B=new WeakMap;function C(a){this.C=a;this.i=[];this.prototype={}}function D(a,b){this.g=b;this.p=a;this.a=!1;this.l=void 0}D.prototype.y=function(a,b,f,h){this.a?this.l=h:this.g[this.p]=h};function E(a,b){a.f=b;return a.z[a.g[a.q+0]++]} +function K(a,b){var f=a.e;if(f){var h=a.g;a=b+h[b+6];for(b=h[b+7];b>>13)&65535);return b} +function S(a,b,f,h,m,k,l,d){var e=m.C,p=a.g,r=a.n,n=8+e.j,q=r+n;for(a.x=h;p.length=a.z.length)break;b[f+0]=l+1;l=a.z[l];try{var d=b[f+7];switch(l){case 0:var e=E(a,{});b[d+e]=L(a);break;case 1:e=E(a,4);b[d+e]=E(a,[]);break;case 2:e=E(a,"x");var p=L(a);if(!(p in a.h))throw new ReferenceError(`${p} is not defined`);b[d+e]=a.h[p];break;case 3:e=E(a,"y");b[d+e]=b[f+4].i[E(a,"y")].E(78,[],void 0,[]);break;case 4:e=E(a,[]);b[d+e]=b[f+3];break;case 5:e=E(a,[]);b[d+e]=b[d+E(a,43)];break;case 6:a.h[L(a)]= +b[d+E(a,[])];break;case 7:var r=E(a,{});b[f+4].i[r].y([],{},[],b[d+E(a,"y")],{});break;case 8:e=E(a,null);var n=b[d+E(a,29)],q=b[d+E(a,8)];b[d+e]=n[q];break;case 9:n=b[d+E(a)];q=b[d+E(a,{})];var y=b[d+E(a,[])];Reflect.set(n,q,y);break;case 10:e=E(a,{});n=b[d+E(a,null)];q=b[d+E(a,[])];b[d+e]=delete n[q];break;case 11:e=E(a,null);var g=b[d+E(a,{})];b[d+e]=g+b[d+E(a,null)];break;case 12:e=E(a,4);g=b[d+E(a,[])];b[d+e]=g-b[d+E(a,"k")];break;case 13:e=E(a,null);g=b[d+E(a)];b[d+e]=g*b[d+E(a)];break;case 14:e= +E(a);g=b[d+E(a,74)];b[d+e]=g/b[d+E(a)];break;case 15:e=E(a,[]);g=b[d+E(a,34)];b[d+e]=g%b[d+E(a,[])];break;case 60:e=E(a,"y");g=b[d+E(a,{})];b[d+e]=Math.pow(g,b[d+E(a)]);break;case 16:e=E(a,null);g=b[d+E(a,null)];b[d+e]=g&b[d+E(a,null)];break;case 17:e=E(a,null);g=b[d+E(a)];b[d+e]=g|b[d+E(a,[])];break;case 18:e=E(a,44);g=b[d+E(a,"x")];b[d+e]=g^b[d+E(a,0)];break;case 19:e=E(a,null);g=b[d+E(a,"k")];b[d+e]=g<>b[d+E(a,null)];break;case 21:e= +E(a,62);g=b[d+E(a,[])];b[d+e]=g>>>b[d+E(a,null)];break;case 22:e=E(a,"k");g=b[d+E(a,null)];b[d+e]=gb[d+E(a,"z")];break;case 24:e=E(a,[]);g=b[d+E(a,89)];b[d+e]=g<=b[d+E(a,[])];break;case 25:e=E(a,{});g=b[d+E(a,12)];b[d+e]=g>=b[d+E(a,[])];break;case 26:e=E(a);g=b[d+E(a,[])];b[d+e]=g===b[d+E(a,[])];break;case 27:e=E(a,{});g=b[d+E(a,[])];b[d+e]=g!==b[d+E(a,14)];break;case 28:e=E(a,"z");g=b[d+E(a,{})];b[d+e]=g==b[d+E(a,[])];break;case 29:e= +E(a,[]);g=b[d+E(a,null)];b[d+e]=g!=b[d+E(a,null)];break;case 30:e=E(a);g=b[d+E(a,null)];b[d+e]=g in b[d+E(a,{})];break;case 31:e=E(a,[]);n=b[d+E(a,[])];b[d+e]=n instanceof b[d+E(a,"k")];break;case 32:e=E(a,"z");b[d+e]=-b[d+E(a,39)];break;case 33:e=E(a,[]);b[d+e]=+b[d+E(a,{})];break;case 34:e=E(a,"y");b[d+e]=!b[d+E(a,68)];break;case 35:e=E(a,null);b[d+e]=~b[d+E(a,71)];break;case 36:e=E(a,null);b[d+e]=typeof b[d+E(a,{})];break;case 37:e=E(a,"y");E(a,[]);b[d+e]=void 0;break;case 38:e=E(a);var V=L(a); +y=Object.prototype.hasOwnProperty.call(a.h,V)?a.h[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=E(a,40);break;case 40:var M=E(a),N=E(a);b[d+M]||(b[f+0]=N);break;case 41:M=E(a,[]);N=E(a,null);b[d+M]&&(b[f+0]=N);break;case 42:e=E(a);var t=b[d+E(a,null)],u=E(a,{});if(u===65535)m=b[d+E(a,[])];else for(m=Array(u),k=0;k>1):0]=F;break;case 46:throw b[d+E(a,"q")];case 47:e=E(a,{});var ja=E(a,[]),ka=E(a,17),la=E(a,[]),Y=E(a,[]),ma=E(a,null),P=Array(Y);for(k=0;k=I.o.length?b[f+0]=sa:b[d+e]=I.o[I.u++];break;case 54:var w=b[f+5];w||(b[f+5]=w=[]);w.push({D:E(a,[]),b:E(a)});break;case 55:b[f+5].pop();break;case 59:(w=b[f+5])||(b[f+5]=w=[]);w.push({A:E(a),B:E(a),H:E(a,"z"),F:E(a)});break;case 56:var ea=E(a),fa=E(a,{}),ta=E(a),J=E(a,{})^ea|0;for(f=fa;f>>13)>>>0;break;case 58:b[f+0]=b[d+E(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+ +l+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(l=a.q;l;){var x=b[l+5];if(x&&x.length>0){f=l;break}K(a,l);a.n=l;l=b[l+1];a.q=l}if(!f)throw z;l=b[f+5].pop();x=b[f+7];l.b!==void 0?(b[x+l.b]=z,b[f+0]=l.D):(b[x+l.B]=l.F,b[x+l.H]=z,b[f+0]=l.A);a.n=f+b[f+6];a.q=f}if(!a.q)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new c({},{},U,[4,1,2,2,0,0,0,3,1,0,0,4,2,0,0,5,3,0,13,6,4,5,0,4,4,0,11,5,6,4,0,4,5,0,15,6,5,4,9,2,3,6,0,2,6,0,45,2],["window","TEST_OUTPUT",6,7,3,5,void 0]),void 0,void 0,new C({k:0,j:7,v:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--174.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--174.js new file mode 100644 index 00000000..784492cf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic--minifyClassObfuscation--174.js @@ -0,0 +1,17 @@ +function b(a,d,h,f,l,k){this.j=d;this.w=a;this.i=k;this.E=null;this.r=f;this.g=[];this.z=l&&h;this.m=1;this.h=0}function x(a){this.F=a;this.l=[];this.prototype={}}function ba(a,d){this.g=a;this.q=d;this.b=!1;this.a=void 0}function ca(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(d){return d.charCodeAt(0)})}var B=new WeakMap;function da(a){return a.b?a.a:a.g[a.q]} +b.prototype.c=function(a,d,h,f,l,k){d=a.F;l=this.g;for(var c=this.m,e=8+d.n,t=c+e;l.length>>13)&65535);return h}; +function fa(a,d){a.b?a.a=d:a.g[a.q]=d}function O(a){return a.j[a.g[a.h+0]++]} +b.prototype.t=function(a,d,h,f,l){var k;this.f=d;for(this.c(l,void 0,0,h,"x",a,void 0);;){d=this.h;a=this.g;f=a[d+0];if(f>=this.j.length)break;a[d+0]=f+1;f=this.j[f];try{var c=a[d+7];switch(f){case 0:var e=O(this);a[c+e]=this.k();break;case 1:e=O(this);a[c+e]=O(this);break;case 2:e=O(this);var t=this.k();if(!(t in this.i))throw new ReferenceError(`${t} is not defined`);a[c+e]=this.i[t];break;case 3:e=O(this);a[c+e]=da(a[d+4].l[O(this)]);break;case 4:e=O(this);a[c+e]=a[d+3];break;case 5:e=O(this); +a[c+e]=a[c+O(this)];break;case 6:this.i[this.k()]=a[c+O(this)];break;case 7:var p=O(this);fa(a[d+4].l[p],a[c+O(this)]);break;case 8:e=O(this);var m=a[c+O(this)],n=a[c+O(this)];a[c+e]=m[n];break;case 9:m=a[c+O(this)];n=a[c+O(this)];var y=a[c+O(this)];Reflect.set(m,n,y);break;case 10:e=O(this);m=a[c+O(this)];n=a[c+O(this)];a[c+e]=delete m[n];break;case 11:e=O(this);var g=a[c+O(this)];a[c+e]=g+a[c+O(this)];break;case 12:e=O(this);g=a[c+O(this)];a[c+e]=g-a[c+O(this)];break;case 13:e=O(this);g=a[c+O(this)]; +a[c+e]=g*a[c+O(this)];break;case 14:e=O(this);g=a[c+O(this)];a[c+e]=g/a[c+O(this)];break;case 15:e=O(this);g=a[c+O(this)];a[c+e]=g%a[c+O(this)];break;case 60:e=O(this);g=a[c+O(this)];a[c+e]=Math.pow(g,a[c+O(this)]);break;case 16:e=O(this);g=a[c+O(this)];a[c+e]=g&a[c+O(this)];break;case 17:e=O(this);g=a[c+O(this)];a[c+e]=g|a[c+O(this)];break;case 18:e=O(this);g=a[c+O(this)];a[c+e]=g^a[c+O(this)];break;case 19:e=O(this);g=a[c+O(this)];a[c+e]=g<>a[c+O(this)];break;case 21:e=O(this);g=a[c+O(this)];a[c+e]=g>>>a[c+O(this)];break;case 22:e=O(this);g=a[c+O(this)];a[c+e]=ga[c+O(this)];break;case 24:e=O(this);g=a[c+O(this)];a[c+e]=g<=a[c+O(this)];break;case 25:e=O(this);g=a[c+O(this)];a[c+e]=g>=a[c+O(this)];break;case 26:e=O(this);g=a[c+O(this)];a[c+e]=g===a[c+O(this)];break;case 27:e=O(this);g=a[c+O(this)];a[c+e]=g!==a[c+O(this)];break;case 28:e=O(this);g=a[c+O(this)];a[c+e]=g== +a[c+O(this)];break;case 29:e=O(this);g=a[c+O(this)];a[c+e]=g!=a[c+O(this)];break;case 30:e=O(this);g=a[c+O(this)];a[c+e]=g in a[c+O(this)];break;case 31:e=O(this);m=a[c+O(this)];a[c+e]=m instanceof a[c+O(this)];break;case 32:e=O(this);a[c+e]=-a[c+O(this)];break;case 33:e=O(this);a[c+e]=+a[c+O(this)];break;case 34:e=O(this);a[c+e]=!a[c+O(this)];break;case 35:e=O(this);a[c+e]=~a[c+O(this)];break;case 36:e=O(this);a[c+e]=typeof a[c+O(this)];break;case 37:e=O(this);O(this);a[c+e]=void 0;break;case 38:e= +O(this);var Q=this.k();y=Object.prototype.hasOwnProperty.call(this.i,Q)?this.i[Q]:void 0;a[c+e]=typeof y;break;case 39:a[d+0]=O(this);break;case 40:var I=O(this),J=O(this);a[c+I]||(a[d+0]=J);break;case 41:I=O(this);J=O(this);a[c+I]&&(a[d+0]=J);break;case 42:e=O(this);var q=a[c+O(this)],r=O(this);if(r===65535)h=a[c+O(this)];else for(h=Array(r),k=0;k>1):0]=D;break;case 46:throw a[c+O(this)];case 47:e=O(this);var ia=O(this),ja=O(this),ka=O(this),T=O(this),la=O(this),L=Array(T);for(k=0;k=G.x.length?a[d+0]=ra:a[c+e]=G.x[G.u++];break;case 54:var v=a[d+5];v||(a[d+5]=v=[]);v.push({e:O(this),A:O(this)});break;case 55:a[d+5].pop();break;case 59:(v=a[d+5])||(a[d+5]=v=[]);v.push({C:O(this),y:O(this),s:O(this),p:O(this)});break;case 56:var Z=O(this),aa=O(this),sa=O(this),H=O(this)^Z|0;for(d=aa;d>>13)>>>0;break;case 58:a[d+0]=a[c+O(this)];break;case 57:debugger;break; +default:throw Error("Unknown opcode: "+f+" at pc "+(a[d+0]-1));}}catch(z){d=0;for(f=this.h;f;){var w=a[f+5];if(w&&w.length>0){d=f;break}C(this,f);this.m=f;this.h=f=a[f+1]}if(!d)throw z;f=a[d+5].pop();w=a[d+7];f.A!==void 0?(a[w+f.A]=z,a[d+0]=f.e):(a[w+f.y]=f.p,a[w+f.s]=z,a[d+0]=f.C);this.m=d+a[d+6];this.h=d}if(!this.h)return this.g[0]}};var P=globalThis;typeof window!=="undefined"&&(P.window=window,P.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(P.module=module,P.exports=typeof exports!=="undefined"?exports:void 0);(new b(["window","TEST_OUTPUT",6,7,3,5,void 0],[4,1,2,2,0,0,0,3,1,0,0,4,2,0,0,5,3,0,13,6,4,5,0,4,4,0,11,5,6,4,0,4,5,0,15,6,5,4,9,2,3,6,0,2,6,0,45,2],[],{},[],P)).t(void 0,void 0,null,void 0,new x({o:0,n:7,v:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic.source.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic.source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/arithmetic.source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--172.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--172.js new file mode 100644 index 00000000..f05f057e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--172.js @@ -0,0 +1,18 @@ +function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var c=new WeakMap;function A(a,g){this.g=g;this.e=a;this.o=!1;this.u=void 0}function B(a,g,k){this.q=k;this.v=a;this.h=g;this.m=null;this.g=[];this.s=1;this.n=0}function C(a,g,k){this.t=k;this.i=[];this.E=[g,a];this.prototype={}}function fa(a,g,k){g=a.g[g+7]+k;k=a.m||(a.m=[]);return k[g]||(k[g]=new A(g,a.g))} +B.prototype.d=function(a,g,k,b,h,f){a=g.t;h=this.g;for(var m=this.s,d=8+a.l,e=m+d;h.length>>13)&65535);return g}function Q(a,g,k,b){var h=a.m;if(h){var f=a.g;a.b=k||b;a=g+f[g+6];for(g=f[g+7];g=a.q.length)break;b[f+0]=m+1;m=a.q[m];try{var d=b[f+7];switch(m){case 0:var e=J(a);b[d+e]=D(a);break;case 1:e=J(a);b[d+e]=J(a);break;case 2:e=J(a);var p=D(a);if(!(p in a.h))throw new ReferenceError(`${p} is not defined`);b[d+e]=a.h[p];break;case 3:e=J(a);b[d+e]=b[f+4].i[J(a)].w(void 0);break;case 4:e=J(a);b[d+e]=b[f+3];break;case 5:e=J(a);b[d+e]=b[d+J(a)];break;case 6:a.h[D(a)]=b[d+J(a)];break;case 7:var ha=J(a); +b[f+4].i[ha].c(void 0,b[d+J(a)],null,"z");break;case 8:e=J(a);var n=b[d+J(a)],q=b[d+J(a)];b[d+e]=n[q];break;case 9:n=b[d+J(a)];q=b[d+J(a)];var x=b[d+J(a)];Reflect.set(n,q,x);break;case 10:e=J(a);n=b[d+J(a)];q=b[d+J(a)];b[d+e]=delete n[q];break;case 11:e=J(a);var l=b[d+J(a)];b[d+e]=l+b[d+J(a)];break;case 12:e=J(a);l=b[d+J(a)];b[d+e]=l-b[d+J(a)];break;case 13:e=J(a);l=b[d+J(a)];b[d+e]=l*b[d+J(a)];break;case 14:e=J(a);l=b[d+J(a)];b[d+e]=l/b[d+J(a)];break;case 15:e=J(a);l=b[d+J(a)];b[d+e]=l%b[d+J(a)]; +break;case 60:e=J(a);l=b[d+J(a)];b[d+e]=Math.pow(l,b[d+J(a)]);break;case 16:e=J(a);l=b[d+J(a)];b[d+e]=l&b[d+J(a)];break;case 17:e=J(a);l=b[d+J(a)];b[d+e]=l|b[d+J(a)];break;case 18:e=J(a);l=b[d+J(a)];b[d+e]=l^b[d+J(a)];break;case 19:e=J(a);l=b[d+J(a)];b[d+e]=l<>b[d+J(a)];break;case 21:e=J(a);l=b[d+J(a)];b[d+e]=l>>>b[d+J(a)];break;case 22:e=J(a);l=b[d+J(a)];b[d+e]=lb[d+J(a)];break;case 24:e=J(a); +l=b[d+J(a)];b[d+e]=l<=b[d+J(a)];break;case 25:e=J(a);l=b[d+J(a)];b[d+e]=l>=b[d+J(a)];break;case 26:e=J(a);l=b[d+J(a)];b[d+e]=l===b[d+J(a)];break;case 27:e=J(a);l=b[d+J(a)];b[d+e]=l!==b[d+J(a)];break;case 28:e=J(a);l=b[d+J(a)];b[d+e]=l==b[d+J(a)];break;case 29:e=J(a);l=b[d+J(a)];b[d+e]=l!=b[d+J(a)];break;case 30:e=J(a);l=b[d+J(a)];b[d+e]=l in b[d+J(a)];break;case 31:e=J(a);n=b[d+J(a)];b[d+e]=n instanceof b[d+J(a)];break;case 32:e=J(a);b[d+e]=-b[d+J(a)];break;case 33:e=J(a);b[d+e]=+b[d+J(a)];break; +case 34:e=J(a);b[d+e]=!b[d+J(a)];break;case 35:e=J(a);b[d+e]=~b[d+J(a)];break;case 36:e=J(a);b[d+e]=typeof b[d+J(a)];break;case 37:e=J(a);J(a);b[d+e]=void 0;break;case 38:e=J(a);var T=D(a);x=Object.prototype.hasOwnProperty.call(a.h,T)?a.h[T]:void 0;b[d+e]=typeof x;break;case 39:b[f+0]=J(a);break;case 40:var K=J(a),L=J(a);b[d+K]||(b[f+0]=L);break;case 41:K=J(a);L=J(a);b[d+K]&&(b[f+0]=L);break;case 42:e=J(a);var r=b[d+J(a)],t=J(a);if(t===65535)k=b[d+J(a)];else for(k=Array(t),h=0;h>1):0]=E;break;case 46:throw b[d+J(a)];case 47:e=J(a);var ja=J(a),ka=J(a),la=J(a),W=J(a),ma=J(a),N=Array(W);for(h=0;h=H.x.length?b[f+0]=sa:b[d+e]=H.x[H.B++];break;case 54:var v=b[f+5];v||(b[f+5]=v=[]);v.push({F:J(a),A:J(a)});break;case 55:b[f+5].pop();break;case 59:(v=b[f+5])||(b[f+5]=v=[]);v.push({k:J(a),C:J(a),f:J(a),D:J(a)});break;case 56:var ca=J(a),da=J(a),ta=J(a),I=J(a)^ca|0;for(f=da;f>>13)>>>0;break;case 58:b[f+0]=b[d+J(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+m+ +" at pc "+(b[f+0]-1));}}catch(y){f=0;for(m=a.n;m;){var w=b[m+5];if(w&&w.length>0){f=m;break}Q(a,m,[],{});a.s=m;m=b[m+1];a.n=m}if(!f)throw y;m=b[f+5].pop();w=b[f+7];m.A!==void 0?(b[w+m.A]=y,b[f+0]=m.F):(b[w+m.C]=m.D,b[w+m.f]=y,b[f+0]=m.k);a.s=f+b[f+6];a.n=f}if(!a.n)return a.g[0]}}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new B(["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],S,[4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40, +8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237,2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7]),new C(35,void 0,{j:0,l:12, +p:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--173.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--173.js new file mode 100644 index 00000000..2a683dd8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--173.js @@ -0,0 +1,18 @@ +function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}function c(a,b,f,h,m){this.z=h;this.r=m;this.h=f;this.e=null;this.g=[];this.w=b;this.c=a;this.n=1;this.q=0}var B=new WeakMap;function C(a){this.C=a;this.i=[];this.prototype={}}function D(a,b){this.g=b;this.p=a;this.a=!1;this.l=void 0}D.prototype.y=function(a,b,f,h){this.a?this.l=h:this.g[this.p]=h};function E(a,b){a.f=b;return a.z[a.g[a.q+0]++]} +function K(a,b){var f=a.e;if(f){var h=a.g;a=b+h[b+6];for(b=h[b+7];b>>13)&65535);return b} +function S(a,b,f,h,m,k,l,d){var e=m.C,p=a.g,r=a.n,n=8+e.j,q=r+n;for(a.x=h;p.length=a.z.length)break;b[f+0]=l+1;l=a.z[l];try{var d=b[f+7];switch(l){case 0:var e=E(a,{});b[d+e]=L(a);break;case 1:e=E(a,4);b[d+e]=E(a,[]);break;case 2:e=E(a,"x");var p=L(a);if(!(p in a.h))throw new ReferenceError(`${p} is not defined`);b[d+e]=a.h[p];break;case 3:e=E(a,"y");b[d+e]=b[f+4].i[E(a,"y")].E(78,[],void 0,[]);break;case 4:e=E(a,[]);b[d+e]=b[f+3];break;case 5:e=E(a,[]);b[d+e]=b[d+E(a,43)];break;case 6:a.h[L(a)]= +b[d+E(a,[])];break;case 7:var r=E(a,{});b[f+4].i[r].y([],{},[],b[d+E(a,"y")],{});break;case 8:e=E(a,null);var n=b[d+E(a,29)],q=b[d+E(a,8)];b[d+e]=n[q];break;case 9:n=b[d+E(a)];q=b[d+E(a,{})];var y=b[d+E(a,[])];Reflect.set(n,q,y);break;case 10:e=E(a,{});n=b[d+E(a,null)];q=b[d+E(a,[])];b[d+e]=delete n[q];break;case 11:e=E(a,null);var g=b[d+E(a,{})];b[d+e]=g+b[d+E(a,null)];break;case 12:e=E(a,4);g=b[d+E(a,[])];b[d+e]=g-b[d+E(a,"k")];break;case 13:e=E(a,null);g=b[d+E(a)];b[d+e]=g*b[d+E(a)];break;case 14:e= +E(a);g=b[d+E(a,74)];b[d+e]=g/b[d+E(a)];break;case 15:e=E(a,[]);g=b[d+E(a,34)];b[d+e]=g%b[d+E(a,[])];break;case 60:e=E(a,"y");g=b[d+E(a,{})];b[d+e]=Math.pow(g,b[d+E(a)]);break;case 16:e=E(a,null);g=b[d+E(a,null)];b[d+e]=g&b[d+E(a,null)];break;case 17:e=E(a,null);g=b[d+E(a)];b[d+e]=g|b[d+E(a,[])];break;case 18:e=E(a,44);g=b[d+E(a,"x")];b[d+e]=g^b[d+E(a,0)];break;case 19:e=E(a,null);g=b[d+E(a,"k")];b[d+e]=g<>b[d+E(a,null)];break;case 21:e= +E(a,62);g=b[d+E(a,[])];b[d+e]=g>>>b[d+E(a,null)];break;case 22:e=E(a,"k");g=b[d+E(a,null)];b[d+e]=gb[d+E(a,"z")];break;case 24:e=E(a,[]);g=b[d+E(a,89)];b[d+e]=g<=b[d+E(a,[])];break;case 25:e=E(a,{});g=b[d+E(a,12)];b[d+e]=g>=b[d+E(a,[])];break;case 26:e=E(a);g=b[d+E(a,[])];b[d+e]=g===b[d+E(a,[])];break;case 27:e=E(a,{});g=b[d+E(a,[])];b[d+e]=g!==b[d+E(a,14)];break;case 28:e=E(a,"z");g=b[d+E(a,{})];b[d+e]=g==b[d+E(a,[])];break;case 29:e= +E(a,[]);g=b[d+E(a,null)];b[d+e]=g!=b[d+E(a,null)];break;case 30:e=E(a);g=b[d+E(a,null)];b[d+e]=g in b[d+E(a,{})];break;case 31:e=E(a,[]);n=b[d+E(a,[])];b[d+e]=n instanceof b[d+E(a,"k")];break;case 32:e=E(a,"z");b[d+e]=-b[d+E(a,39)];break;case 33:e=E(a,[]);b[d+e]=+b[d+E(a,{})];break;case 34:e=E(a,"y");b[d+e]=!b[d+E(a,68)];break;case 35:e=E(a,null);b[d+e]=~b[d+E(a,71)];break;case 36:e=E(a,null);b[d+e]=typeof b[d+E(a,{})];break;case 37:e=E(a,"y");E(a,[]);b[d+e]=void 0;break;case 38:e=E(a);var V=L(a); +y=Object.prototype.hasOwnProperty.call(a.h,V)?a.h[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=E(a,40);break;case 40:var M=E(a),N=E(a);b[d+M]||(b[f+0]=N);break;case 41:M=E(a,[]);N=E(a,null);b[d+M]&&(b[f+0]=N);break;case 42:e=E(a);var t=b[d+E(a,null)],u=E(a,{});if(u===65535)m=b[d+E(a,[])];else for(m=Array(u),k=0;k>1):0]=F;break;case 46:throw b[d+E(a,"q")];case 47:e=E(a,{});var ja=E(a,[]),ka=E(a,17),la=E(a,[]),Y=E(a,[]),ma=E(a,null),P=Array(Y);for(k=0;k=I.o.length?b[f+0]=sa:b[d+e]=I.o[I.u++];break;case 54:var w=b[f+5];w||(b[f+5]=w=[]);w.push({D:E(a,[]),b:E(a)});break;case 55:b[f+5].pop();break;case 59:(w=b[f+5])||(b[f+5]=w=[]);w.push({A:E(a),B:E(a),H:E(a,"z"),F:E(a)});break;case 56:var ea=E(a),fa=E(a,{}),ta=E(a),J=E(a,{})^ea|0;for(f=fa;f>>13)>>>0;break;case 58:b[f+0]=b[d+E(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+ +l+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(l=a.q;l;){var x=b[l+5];if(x&&x.length>0){f=l;break}K(a,l);a.n=l;l=b[l+1];a.q=l}if(!f)throw z;l=b[f+5].pop();x=b[f+7];l.b!==void 0?(b[x+l.b]=z,b[f+0]=l.D):(b[x+l.B]=l.F,b[x+l.H]=z,b[f+0]=l.A);a.n=f+b[f+6];a.q=f}if(!a.q)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new c({},{},U,[4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185,2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7, +40,8,237,2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"]),void 0,void 0,new C({k:0, +j:12,v:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--174.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--174.js new file mode 100644 index 00000000..eb9acc05 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes--minifyClassObfuscation--174.js @@ -0,0 +1,20 @@ +function b(a,d,h,f,l,k){this.j=d;this.w=a;this.i=k;this.E=null;this.r=f;this.g=[];this.z=l&&h;this.m=1;this.h=0}function x(a){this.F=a;this.l=[];this.prototype={}}function ba(a,d){this.g=a;this.q=d;this.b=!1;this.a=void 0}function ca(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(d){return d.charCodeAt(0)})}var B=new WeakMap;function da(a){return a.b?a.a:a.g[a.q]} +b.prototype.c=function(a,d,h,f,l,k){d=a.F;l=this.g;for(var c=this.m,e=8+d.n,t=c+e;l.length>>13)&65535);return h}; +function fa(a,d){a.b?a.a=d:a.g[a.q]=d}function O(a){return a.j[a.g[a.h+0]++]} +b.prototype.t=function(a,d,h,f,l){var k;this.f=d;for(this.c(l,void 0,0,h,"x",a,void 0);;){d=this.h;a=this.g;f=a[d+0];if(f>=this.j.length)break;a[d+0]=f+1;f=this.j[f];try{var c=a[d+7];switch(f){case 0:var e=O(this);a[c+e]=this.k();break;case 1:e=O(this);a[c+e]=O(this);break;case 2:e=O(this);var t=this.k();if(!(t in this.i))throw new ReferenceError(`${t} is not defined`);a[c+e]=this.i[t];break;case 3:e=O(this);a[c+e]=da(a[d+4].l[O(this)]);break;case 4:e=O(this);a[c+e]=a[d+3];break;case 5:e=O(this); +a[c+e]=a[c+O(this)];break;case 6:this.i[this.k()]=a[c+O(this)];break;case 7:var p=O(this);fa(a[d+4].l[p],a[c+O(this)]);break;case 8:e=O(this);var m=a[c+O(this)],n=a[c+O(this)];a[c+e]=m[n];break;case 9:m=a[c+O(this)];n=a[c+O(this)];var y=a[c+O(this)];Reflect.set(m,n,y);break;case 10:e=O(this);m=a[c+O(this)];n=a[c+O(this)];a[c+e]=delete m[n];break;case 11:e=O(this);var g=a[c+O(this)];a[c+e]=g+a[c+O(this)];break;case 12:e=O(this);g=a[c+O(this)];a[c+e]=g-a[c+O(this)];break;case 13:e=O(this);g=a[c+O(this)]; +a[c+e]=g*a[c+O(this)];break;case 14:e=O(this);g=a[c+O(this)];a[c+e]=g/a[c+O(this)];break;case 15:e=O(this);g=a[c+O(this)];a[c+e]=g%a[c+O(this)];break;case 60:e=O(this);g=a[c+O(this)];a[c+e]=Math.pow(g,a[c+O(this)]);break;case 16:e=O(this);g=a[c+O(this)];a[c+e]=g&a[c+O(this)];break;case 17:e=O(this);g=a[c+O(this)];a[c+e]=g|a[c+O(this)];break;case 18:e=O(this);g=a[c+O(this)];a[c+e]=g^a[c+O(this)];break;case 19:e=O(this);g=a[c+O(this)];a[c+e]=g<>a[c+O(this)];break;case 21:e=O(this);g=a[c+O(this)];a[c+e]=g>>>a[c+O(this)];break;case 22:e=O(this);g=a[c+O(this)];a[c+e]=ga[c+O(this)];break;case 24:e=O(this);g=a[c+O(this)];a[c+e]=g<=a[c+O(this)];break;case 25:e=O(this);g=a[c+O(this)];a[c+e]=g>=a[c+O(this)];break;case 26:e=O(this);g=a[c+O(this)];a[c+e]=g===a[c+O(this)];break;case 27:e=O(this);g=a[c+O(this)];a[c+e]=g!==a[c+O(this)];break;case 28:e=O(this);g=a[c+O(this)];a[c+e]=g== +a[c+O(this)];break;case 29:e=O(this);g=a[c+O(this)];a[c+e]=g!=a[c+O(this)];break;case 30:e=O(this);g=a[c+O(this)];a[c+e]=g in a[c+O(this)];break;case 31:e=O(this);m=a[c+O(this)];a[c+e]=m instanceof a[c+O(this)];break;case 32:e=O(this);a[c+e]=-a[c+O(this)];break;case 33:e=O(this);a[c+e]=+a[c+O(this)];break;case 34:e=O(this);a[c+e]=!a[c+O(this)];break;case 35:e=O(this);a[c+e]=~a[c+O(this)];break;case 36:e=O(this);a[c+e]=typeof a[c+O(this)];break;case 37:e=O(this);O(this);a[c+e]=void 0;break;case 38:e= +O(this);var Q=this.k();y=Object.prototype.hasOwnProperty.call(this.i,Q)?this.i[Q]:void 0;a[c+e]=typeof y;break;case 39:a[d+0]=O(this);break;case 40:var I=O(this),J=O(this);a[c+I]||(a[d+0]=J);break;case 41:I=O(this);J=O(this);a[c+I]&&(a[d+0]=J);break;case 42:e=O(this);var q=a[c+O(this)],r=O(this);if(r===65535)h=a[c+O(this)];else for(h=Array(r),k=0;k>1):0]=D;break;case 46:throw a[c+O(this)];case 47:e=O(this);var ia=O(this),ja=O(this),ka=O(this),T=O(this),la=O(this),L=Array(T);for(k=0;k=G.x.length?a[d+0]=ra:a[c+e]=G.x[G.u++];break;case 54:var v=a[d+5];v||(a[d+5]=v=[]);v.push({e:O(this),A:O(this)});break;case 55:a[d+5].pop();break;case 59:(v=a[d+5])||(a[d+5]=v=[]);v.push({C:O(this),y:O(this),s:O(this),p:O(this)});break;case 56:var Z=O(this),aa=O(this),sa=O(this),H=O(this)^Z|0;for(d=aa;d>>13)>>>0;break;case 58:a[d+0]=a[c+O(this)];break;case 57:debugger;break; +default:throw Error("Unknown opcode: "+f+" at pc "+(a[d+0]-1));}}catch(z){d=0;for(f=this.h;f;){var w=a[f+5];if(w&&w.length>0){d=f;break}C(this,f);this.m=f;this.h=f=a[f+1]}if(!d)throw z;f=a[d+5].pop();w=a[d+7];f.A!==void 0?(a[w+f.A]=z,a[d+0]=f.e):(a[w+f.y]=f.p,a[w+f.s]=z,a[d+0]=f.C);this.m=d+a[d+6];this.h=d}if(!this.h)return this.g[0]}};var P=globalThis;typeof window!=="undefined"&&(P.window=window,P.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(P.module=module,P.exports=typeof exports!=="undefined"?exports:void 0); +(new b(["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],[4,1,47,5,89,0,4,1,0,1,2,5,3,5,48,5,0,5,2,5,42,5,3,0,5,4,5,2,5,0,0,0,6,1,0,0,7,2,0,42,8,4,1,7,0,7,3,0,42,9,4,1,7,0,7,4,0,42,10,4,1,7,0,7,5,0,42,11,4,1,7,48,7,5,8,9,10,11,2,9,5,6,7,0,5,6,0,45,5,4,1,0,3,2,0,5,2,3,47,3,117,1,12,2,0,0,0,1,2,45,3,0,3,6,0,45,3,4,2,59,326,4,5,353,54,264,3,3,7,0,0,8,7,0,8,9,7,8,0,8,8,0,11,10,8,0,43,8,7,9,1,10,0,7,2,0,26,8,0,7,40,8,185, +2,7,9,0,0,8,10,0,8,9,7,8,0,8,11,0,43,10,7,9,1,8,0,7,3,0,26,8,0,7,40,8,211,0,7,12,0,5,6,7,0,7,13,0,8,8,6,7,0,7,4,0,26,8,0,7,40,8,237,2,7,14,0,0,8,15,0,44,9,7,1,8,46,9,3,7,1,0,8,3,0,11,9,7,8,7,1,9,55,5,5,9,1,4,355,55,39,326,55,39,320,3,7,0,0,8,7,0,8,9,7,8,0,8,16,0,0,10,17,0,8,11,3,10,11,10,8,11,43,8,7,9,1,10,3,7,1,0,8,3,0,11,9,7,8,7,1,9,5,5,9,1,4,357,55,39,326,55,1,4,359,39,326,3,7,0,0,8,7,0,8,9,7,8,0,8,18,0,11,10,8,0,43,8,7,9,1,10,58,4,46,5,45,5,45,5,0,7,6,0,45,7],[],{},[],P)).t(void 0,void 0,null, +void 0,new x({o:0,n:12,v:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes.source.js b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes.source.js new file mode 100644 index 00000000..7b4d094e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/exceptionRoutes.source.js @@ -0,0 +1 @@ +const trace = []; function make() { let count = 0; return function run(mode) { try { trace.push("try:" + mode); if (mode === 0) JSON.parse("{"); if (mode === 1) { const value = null; value.missing; } if (mode === 2) throw new Error("explicit"); return ++count; } catch (error) { trace.push("catch:" + error.name); return ++count; } finally { trace.push("finally:" + mode); } }; } const run = make(); window.TEST_OUTPUT = [run(0), run(1), run(2), run(3), trace]; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/provenance.json b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/provenance.json new file mode 100644 index 00000000..81578b4e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-class-frontend/provenance.json @@ -0,0 +1,414 @@ +{ + "schemaVersion": "jsconfuser-vm-parser-fixtures.v1", + "population": "fresh-172-174", + "encoderPin": "20c5b96bf57337c568579352758d7685358650de", + "evidence": "Frozen source and encoded data for source-AST admission/effect/control tests. No native qualification is asserted.", + "stripping": "Leading emitted comment lines removed; complete raw encodes and generation journals retained separately by the supplier.", + "records": [ + { + "id": "arithmetic--baseline--172", + "case": "arithmetic", + "profile": "baseline", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--baseline--172.js" + }, + { + "id": "arithmetic--baseline--173", + "case": "arithmetic", + "profile": "baseline", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--baseline--173.js" + }, + { + "id": "arithmetic--baseline--174", + "case": "arithmetic", + "profile": "baseline", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--baseline--174.js" + }, + { + "id": "arithmetic--minify--172", + "case": "arithmetic", + "profile": "minify", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--minify--172.js" + }, + { + "id": "arithmetic--minify--173", + "case": "arithmetic", + "profile": "minify", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--minify--173.js" + }, + { + "id": "arithmetic--minify--174", + "case": "arithmetic", + "profile": "minify", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--minify--174.js" + }, + { + "id": "arithmetic--classObfuscation--172", + "case": "arithmetic", + "profile": "classObfuscation", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--classObfuscation--172.js" + }, + { + "id": "arithmetic--classObfuscation--173", + "case": "arithmetic", + "profile": "classObfuscation", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--classObfuscation--173.js" + }, + { + "id": "arithmetic--classObfuscation--174", + "case": "arithmetic", + "profile": "classObfuscation", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--classObfuscation--174.js" + }, + { + "id": "arithmetic--minifyClassObfuscation--172", + "case": "arithmetic", + "profile": "minifyClassObfuscation", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--minifyClassObfuscation--172.js" + }, + { + "id": "arithmetic--minifyClassObfuscation--173", + "case": "arithmetic", + "profile": "minifyClassObfuscation", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--minifyClassObfuscation--173.js" + }, + { + "id": "arithmetic--minifyClassObfuscation--174", + "case": "arithmetic", + "profile": "minifyClassObfuscation", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "arithmetic.source.js", + "encoded": "arithmetic--minifyClassObfuscation--174.js" + }, + { + "id": "exceptionRoutes--minifyClassObfuscation--172", + "case": "exceptionRoutes", + "profile": "minifyClassObfuscation", + "seed": 172, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "exceptionRoutes.source.js", + "encoded": "exceptionRoutes--minifyClassObfuscation--172.js" + }, + { + "id": "exceptionRoutes--minifyClassObfuscation--173", + "case": "exceptionRoutes", + "profile": "minifyClassObfuscation", + "seed": 173, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "exceptionRoutes.source.js", + "encoded": "exceptionRoutes--minifyClassObfuscation--173.js" + }, + { + "id": "exceptionRoutes--minifyClassObfuscation--174", + "case": "exceptionRoutes", + "profile": "minifyClassObfuscation", + "seed": 174, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "source": "exceptionRoutes.source.js", + "encoded": "exceptionRoutes--minifyClassObfuscation--174.js" + } + ] +} diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-181.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-181.js new file mode 100644 index 00000000..a19a86e6 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-181.js @@ -0,0 +1,960 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(fake_14, bytecode, constants, fake_13, fake_15, globals) { + this.v = bytecode; + this.X = constants; + this.ac = fake_14; + this.Y = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.N = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.f = []; + this.R = 11; + this.F = fake_15 || fake_13; + this.P = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +function Upvalue(absSlot, fake_3, regs, fake_2, fake_1) { + this.n = fake_3; + this.f = regs; // shared reference to VM._regs flat array + this.r = fake_2; + this.i = absSlot; // absolute index; stable as long as frame is alive + this.J = false; + this.o = undefined; + this.M = fake_1; +} +var BYTECODE = [2151, 1, 56464, 2, 0, 0, 26188, 3, 1, 0, 26188, 4, 2, 0, 26188, 5, 3, 0, 64040, 6, 4, 5, 26188, 4, 4, 0, 37279, 5, 6, 4, 26188, 4, 5, 0, 64807, 6, 5, 4, 45975, 2, 3, 6, 26188, 2, 6, 0, 62456, 2]; +// Closure & Frame +function Closure(fake_12, fake_11, fn) { + this.l = fn; + this.K = fake_12; + this.C = []; + this.prototype = {}; // <- default prototype object for `new` + this.s = fake_11; +} +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +VM.prototype.aa = function (fake_20, fp, fake_23, fake_22, slot, fake_21) { + this.p = fake_21 || fake_22; + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.f[fp + 7] + slot; + var uvs = this.N || (this.N = []); + this.G = fake_23; + this.Z = fake_20; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, "y", this.f, {}, [])); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.E = function (idxIn, fake_25, fake_24, keyIn) { + this.w = [fake_24, fake_25]; + var idx = idxIn ?? this.S(undefined); + var key = keyIn ?? this.S({}); + var v = this.X[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +Upvalue.prototype.O = function (fake_6, v, fake_7) { + if (this.J) this.o = v;else this.f[this.i] = v; + this.j = fake_7; + this.H = fake_6; +}; +Upvalue.prototype.u = function (fake_10, fake_8, fake_9) { + this.af = fake_10; + this.o = this.f[this.i]; + this.J = true; + this.h = fake_9 || fake_8; +}; +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.ag = function (fake_17, fake_18, fake_19, thisVal, retDst, args, closure) { + var fn = closure.l; + var regs = this.f; + this.W = fake_17; + var fp = this.R; + var size = 13 + fn.q; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 13; + regs[fp + 1] = fn.ab; + regs[fp + 10] = this.P; + this.e = fake_18; + regs[fp + 8] = retDst; + regs[fp + 2] = thisVal; + regs[fp + 3] = closure; + regs[fp + 4] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof 9 === "number") regs[fp + 9] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.U; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.ab; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.d = fake_19; + this.R = end; + if (args) { + if (fn.z) { + var restSlot = fn.U - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.q; a++) regs[base + a] = args[a]; + } + if (fn.U < fn.q) regs[base + fn.U] = args; + } + this.P = fp; +}; +VM.prototype.S = function (fake_16) { + this.k = fake_16; + return this.v[this.f[this.P + 1]++]; +}; +VM.prototype.b = function (fake_27, fake_28, fake_29, fake_26, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.N; + this.Q = fake_27; + if (!uvs) return; // nothing in this program ever captured a local + this.x = fake_29 && fake_26; + var regs = this.f; + this.g = fake_28; + var hi = fp + regs[fp + 4]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.u("q", null, undefined); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.ah = function (fake_4, fake_5) { + this.T = [fake_5, fake_4]; + return this.J ? this.o : this.f[this.i]; +}; +VM.prototype.t = function (closure, args, fake_31, thisVal, fake_30, fake_32) { + this.A = fake_30 || fake_31; + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.ag(null, null, [], thisVal, 0, args, closure); + this.a = fake_32; + while (true) { + var fp = this.P; + var regs = this.f; + var bc = this.v; + var pc = regs[fp + 1]; + if (pc >= bc.length) break; + regs[fp + 1] = pc + 1; + var op = this.v[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof 0 === "number") regs[fp + 0] = regs[fp + 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.v.length; i++) this.v[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.R; i2++) this.f[i2] = undefined; + op = 39919; + regs[fp + 1] = this.v.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 26188: + { + var dst = this.S([]); + regs[base + dst] = this.E(); + break; + } + case 35667: + { + var dst = this.S(74); + regs[base + dst] = this.S([]); + break; + } + case 56464: + { + var dst = this.S("q"); + var globalName = this.E(); + if (!(globalName in this.Y)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.Y[globalName]; + break; + } + case 34526: + { + var dst = this.S("y"); + regs[base + dst] = regs[fp + 3].C[this.S({})].ah(null, undefined); + break; + } + case 2151: + { + var dst = this.S(93); + regs[base + dst] = regs[fp + 2]; + break; + } + case 21164: + { + var dst = this.S({}); + regs[base + dst] = regs[base + this.S({})]; + break; + } + case 4576: + { + // globals[globalName] = regs[src] + this.Y[this.E()] = regs[base + this.S(undefined)]; + break; + } + case 43086: + { + var uvIdx = this.S({}); + regs[fp + 3].C[uvIdx].O(null, regs[base + this.S(67)], "y"); + break; + } + case 63110: + { + // dst = regs[obj][regs[key]] + var dst = this.S(null); + var obj = regs[base + this.S({})]; + var key = regs[base + this.S(75)]; + regs[base + dst] = obj[key]; + break; + } + case 45975: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.S({})]; + var key = regs[base + this.S(23)]; + var val = regs[base + this.S("k")]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 59697: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.S(null); + var obj = regs[base + this.S(43)]; + var key = regs[base + this.S("z")]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 37279: + { + var dst = this.S(undefined); + var a = regs[base + this.S({})]; + regs[base + dst] = a + regs[base + this.S([])]; + break; + } + case 363: + { + var dst = this.S(undefined); + var a = regs[base + this.S([])]; + regs[base + dst] = a - regs[base + this.S({})]; + break; + } + case 64040: + { + var dst = this.S([]); + var a = regs[base + this.S(22)]; + regs[base + dst] = a * regs[base + this.S(null)]; + break; + } + case 64597: + { + var dst = this.S(undefined); + var a = regs[base + this.S([])]; + regs[base + dst] = a / regs[base + this.S([])]; + break; + } + case 64807: + { + var dst = this.S(undefined); + var a = regs[base + this.S({})]; + regs[base + dst] = a % regs[base + this.S("k")]; + break; + } + case 30314: + { + // Math.pow instead of `**` + var dst = this.S("q"); + var a = regs[base + this.S({})]; + regs[base + dst] = Math.pow(a, regs[base + this.S(null)]); + break; + } + case 24955: + { + var dst = this.S(45); + var a = regs[base + this.S(null)]; + regs[base + dst] = a & regs[base + this.S("k")]; + break; + } + case 57871: + { + var dst = this.S(null); + var a = regs[base + this.S("q")]; + regs[base + dst] = a | regs[base + this.S([])]; + break; + } + case 5417: + { + var dst = this.S(undefined); + var a = regs[base + this.S("k")]; + regs[base + dst] = a ^ regs[base + this.S([])]; + break; + } + case 9866: + { + var dst = this.S([]); + var a = regs[base + this.S("x")]; + regs[base + dst] = a << regs[base + this.S(null)]; + break; + } + case 12140: + { + var dst = this.S(30); + var a = regs[base + this.S(null)]; + regs[base + dst] = a >> regs[base + this.S([])]; + break; + } + case 46998: + { + var dst = this.S(73); + var a = regs[base + this.S(31)]; + regs[base + dst] = a >>> regs[base + this.S(undefined)]; + break; + } + + // Comparison (dst, src1, src2) + case 18923: + { + var dst = this.S("q"); + var a = regs[base + this.S("z")]; + regs[base + dst] = a < regs[base + this.S(undefined)]; + break; + } + case 23357: + { + var dst = this.S([]); + var a = regs[base + this.S(undefined)]; + regs[base + dst] = a > regs[base + this.S(null)]; + break; + } + case 64658: + { + var dst = this.S(null); + var a = regs[base + this.S(27)]; + regs[base + dst] = a <= regs[base + this.S(undefined)]; + break; + } + case 29044: + { + var dst = this.S(48); + var a = regs[base + this.S("q")]; + regs[base + dst] = a >= regs[base + this.S(86)]; + break; + } + case 25819: + { + var dst = this.S({}); + var a = regs[base + this.S({})]; + regs[base + dst] = a === regs[base + this.S(null)]; + break; + } + case 39164: + { + var dst = this.S(undefined); + var a = regs[base + this.S({})]; + regs[base + dst] = a !== regs[base + this.S("z")]; + break; + } + case 8215: + { + var dst = this.S([]); + var a = regs[base + this.S(undefined)]; + regs[base + dst] = a == regs[base + this.S([])]; + break; + } + case 31978: + { + var dst = this.S({}); + var a = regs[base + this.S(null)]; + regs[base + dst] = a != regs[base + this.S([])]; + break; + } + case 21914: + { + var dst = this.S(undefined); + var a = regs[base + this.S(null)]; + regs[base + dst] = a in regs[base + this.S([])]; + break; + } + case 6605: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.S(94); + var obj = regs[base + this.S(undefined)]; + regs[base + dst] = obj instanceof regs[base + this.S([])]; + break; + } + + // Unary (dst, src) + case 57697: + { + var dst = this.S(undefined); + regs[base + dst] = -regs[base + this.S([])]; + break; + } + case 23644: + { + var dst = this.S(undefined); + regs[base + dst] = +regs[base + this.S(undefined)]; + break; + } + case 43518: + { + var dst = this.S(null); + regs[base + dst] = !regs[base + this.S([])]; + break; + } + case 17920: + { + var dst = this.S([]); + regs[base + dst] = ~regs[base + this.S({})]; + break; + } + case 39066: + { + var dst = this.S(null); + regs[base + dst] = typeof regs[base + this.S(null)]; + break; + } + case 2548: + { + var dst = this.S(undefined); + this.S(3); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 36661: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.S(undefined); + var name = this.E(); + var val = Object.prototype.hasOwnProperty.call(this.Y, name) ? this.Y[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39919: + regs[fp + 1] = this.S(undefined); + break; + case 37081: + { + var src = this.S(null); + var target = this.S({}); + if (!regs[base + src]) regs[fp + 1] = target; + break; + } + case 5524: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.S("k"); + var target = this.S(null); + if (regs[base + src]) regs[fp + 1] = target; + break; + } + + // Calls + case 4545: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.S(null); + var callee = regs[base + this.S(null)]; + var argc = this.S({}); + var args; + if (argc === 3847166315) { + args = regs[base + this.S(40)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.S([])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.ag({}, null, [], this.Y, dst << 1, args, closure); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 15913: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.S({}); + var receiver = regs[base + this.S(52)]; + var callee = regs[base + this.S(undefined)]; + var argc = this.S(17); + var args; + if (argc === 3847166315) { + args = regs[base + this.S([])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.S(59)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.ag(undefined, null, {}, receiver, dst << 1, args, closure); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 52616: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.S(undefined); + var callee = regs[base + this.S(32)]; + var argc = this.S(null); + var args; + if (argc === 3847166315) { + args = regs[base + this.S({})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.S(undefined)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.ag(undefined, "y", {}, Object.create(closure.prototype || null), dst << 1 | 1, args, closure); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 62456: + { + var retVal = regs[base + this.S("k")]; + this.b({}, 41, undefined, "k", fp); // must happen before frame is abandoned + + var caller = regs[fp + 10]; + var retDst = regs[fp + 8]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 2]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 4]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.R = fp; + this.P = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 19563: + throw regs[base + this.S(undefined)]; + + // Closures + case 18527: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.S("k"); + var startPc = this.S("k"); + var paramCount = this.S(null); + var regCount = this.S("z"); + var uvCount = this.S([]); + var hasRest = this.S({}); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.S("x"); + var uvIndex = this.S(75); + uvDescs[i] = { + V: isLocalRaw, + m: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + U: paramCount, + q: regCount, + ab: startPc, + z: hasRest + }; + var closure = new Closure("z", [], fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.V) { + closure.C.push(this.aa({}, fp, 23, [], uvd.m, [])); + } else { + closure.C.push(regs[fp + 3].C[uvd.m]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(21, self.v, self.X, [], {}, self.Y).t(c, Array.prototype.slice.call(arguments), null, this == null ? self.Y : this, "z", 93); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 28412: + { + // dst, count, [elemReg...] + var dst = this.S(null); + var count = this.S({}); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.S({})]; + regs[base + dst] = elems; + break; + } + case 53382: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.S([]); + var pairCount = this.S(35); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.S({})]; + var val = regs[base + this.S(40)]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 1716: + { + // obj, key, fn + var obj = regs[base + this.S(null)]; + var key = regs[base + this.S("x")]; + var getterFn = regs[base + this.S(null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 28189: + { + // obj, key, fn + var obj = regs[base + this.S(undefined)]; + var key = regs[base + this.S(null)]; + var setterFn = regs[base + this.S(undefined)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52804: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.S([]); + var obj = regs[base + this.S([])]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + D: keys, + c: 0 + }; + break; + } + case 8387: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.S(undefined); + var iter = regs[base + this.S([])]; + var exitTarget = this.S("y"); + if (iter.c >= iter.D.length) { + regs[fp + 1] = exitTarget; + } else { + regs[base + dst] = iter.D[iter.c++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 50691: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 11]; + if (!hs) regs[fp + 11] = hs = []; + hs.push({ + I: this.S("x"), + L: this.S("y") + }); + break; + } + case 40557: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 11].pop(); + break; + } + case 57952: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 11]; + if (!hs) regs[fp + 11] = hs = []; + hs.push({ + y: this.S(null), + ae: this.S(69), + B: this.S(undefined), + ad: this.S(undefined) + }); + break; + } + + // Self-modifying bytecode + case 32453: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.S(undefined); + var sliceStart = this.S("q"); + var sliceEnd = this.S(null); + var pk = this.S([]) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.v[destPc + (pi - sliceStart)] = (this.v[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 13119: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 1] = regs[base + this.S([])]; + break; + } + case 54257: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 1] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.P; + while (searchFrame) { + var handlers = regs[searchFrame + 11]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.b(null, [], [], undefined, searchFrame); + this.R = searchFrame; + searchFrame = regs[searchFrame + 10]; + this.P = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 11].pop(); + var hBase = regs[handledFrame + 7]; + if (h.L !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.L] = err; + regs[handledFrame + 1] = h.I; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.ae] = h.ad; + regs[hBase + h.B] = err; + regs[handledFrame + 1] = h.y; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.R = handledFrame + regs[handledFrame + 4]; + this.P = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.P) return this.f[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(82, decodeBytecode(BYTECODE), CONSTANTS, 31, [], globals); +vm.t(new Closure(undefined, 90, { + U: 0, + q: 7, + ab: 0 +}), null // no arguments object / parameter setup for the root frame +, 45, undefined, [], 33); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-182.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-182.js new file mode 100644 index 00000000..d35b4bef --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-182.js @@ -0,0 +1,952 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) + +// Closure & Frame +function Closure(fn, fake_6) { + this.B = fn; + this.E = []; + this.prototype = {}; // <- default prototype object for `new` + this.O = fake_6; +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function VM(bytecode, fake_7, globals, constants) { + this.J = bytecode; + this.A = constants; + this.U = fake_7; + this.d = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.W = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.T = []; + this.u = 8; + this.o = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +function Upvalue(absSlot, fake_1, regs) { + this.e = fake_1; + this.T = regs; // shared reference to VM._regs flat array + this.f = absSlot; // absolute index; stable as long as frame is alive + this.p = false; + this.j = undefined; +} +var BYTECODE = [49294, 1, 44053, 2, 0, 0, 63273, 3, 1, 0, 63273, 4, 2, 0, 63273, 5, 3, 0, 59734, 6, 4, 5, 63273, 4, 4, 0, 38978, 5, 6, 4, 63273, 4, 5, 0, 11271, 6, 5, 4, 5872, 2, 3, 6, 63273, 2, 6, 0, 13431, 2]; +Upvalue.prototype.i = function (fake_2) { + this.Y = fake_2; + return this.p ? this.j : this.T[this.f]; +}; +Upvalue.prototype.s = function (fake_4, fake_5) { + this.j = this.T[this.f]; + this.r = fake_5; + this.p = true; + this.a = fake_4; +}; +VM.prototype.H = function (fp, fake_17) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.W; + this.N = fake_17; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this.T; + var hi = fp + regs[fp + 4]; + for (var i = regs[fp + 0]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.s(65, undefined); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.y = function (fake_3, v) { + if (this.p) this.j = v;else this.T[this.f] = v; + this.k = fake_3; +}; +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.c = function (fake_16, keyIn, idxIn) { + var idx = idxIn ?? this.L(27, null, null); + var key = keyIn ?? this.L("y", {}, undefined); + this.g = fake_16; + var v = this.A[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.V = function (fake_15, fake_13, fp, fake_14, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.T[fp + 0] + slot; + var uvs = this.W || (this.W = []); + this.w = fake_13; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, undefined, this.T)); + this.t = [fake_15, fake_14]; +}; +VM.prototype.L = function (fake_10, fake_9, fake_8) { + this.n = fake_8; + this.m = fake_9 && fake_10; + return this.J[this.T[this.o + 7]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.I = function (retDst, args, fake_11, fake_12, closure, thisVal) { + var fn = closure.B; + var regs = this.T; + var fp = this.u; + var size = 10 + fn.C; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 10; + regs[fp + 7] = fn.F; + this.G = [fake_11, fake_12]; + regs[fp + 2] = this.o; + regs[fp + 1] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 6] = closure; + regs[fp + 4] = size; + regs[fp + 0] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof 8 === "number") regs[fp + 8] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.X; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.F; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.u = end; + if (args) { + if (fn.z) { + var restSlot = fn.X - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.C; a++) regs[base + a] = args[a]; + } + if (fn.X < fn.C) regs[base + fn.X] = args; + } + this.o = fp; +}; +VM.prototype.q = function (args, fake_18, fake_19, closure, thisVal, fake_20) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.h = [fake_19, fake_20]; + this.I(0, args, 42, {}, closure, thisVal); + while (true) { + var fp = this.o; + var regs = this.T; + var bc = this.J; + var pc = regs[fp + 7]; + if (pc >= bc.length) break; + regs[fp + 7] = pc + 1; + var op = this.J[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.J.length; i++) this.J[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.u; i2++) this.T[i2] = undefined; + op = 7297; + regs[fp + 7] = this.J.length; // jump past end to halt + } + } + try { + var base = regs[fp + 0]; + + /* @SWITCH */ + switch (op) { + case 63273: + { + var dst = this.L("x", "k", {}); + regs[base + dst] = this.c(); + break; + } + case 2068: + { + var dst = this.L({}, undefined, 52); + regs[base + dst] = this.L(null, undefined, "k"); + break; + } + case 44053: + { + var dst = this.L({}, undefined, undefined); + var globalName = this.c(); + if (!(globalName in this.d)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.d[globalName]; + break; + } + case 41765: + { + var dst = this.L(null, null, undefined); + regs[base + dst] = regs[fp + 6].E[this.L(81, null, "x")].i([]); + break; + } + case 49294: + { + var dst = this.L({}, "z", null); + regs[base + dst] = regs[fp + 3]; + break; + } + case 12815: + { + var dst = this.L(null, "k", {}); + regs[base + dst] = regs[base + this.L(null, "k", "z")]; + break; + } + case 28556: + { + // globals[globalName] = regs[src] + this.d[this.c()] = regs[base + this.L({}, {}, null)]; + break; + } + case 55066: + { + var uvIdx = this.L(77, null, null); + regs[fp + 6].E[uvIdx].y(44, regs[base + this.L(undefined, 31, 81)]); + break; + } + case 55279: + { + // dst = regs[obj][regs[key]] + var dst = this.L("q", [], 51); + var obj = regs[base + this.L(undefined, {}, "y")]; + var key = regs[base + this.L(10, 37, [])]; + regs[base + dst] = obj[key]; + break; + } + case 5872: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.L({}, undefined, null)]; + var key = regs[base + this.L(undefined, "z", [])]; + var val = regs[base + this.L(75, 6, {})]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 63910: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.L(null, null, "q"); + var obj = regs[base + this.L(null, null, null)]; + var key = regs[base + this.L(null, undefined, {})]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 38978: + { + var dst = this.L(85, "k", undefined); + var a = regs[base + this.L(null, 85, null)]; + regs[base + dst] = a + regs[base + this.L([], {}, null)]; + break; + } + case 1702: + { + var dst = this.L("k", undefined, []); + var a = regs[base + this.L(null, {}, {})]; + regs[base + dst] = a - regs[base + this.L("y", undefined, 28)]; + break; + } + case 59734: + { + var dst = this.L("z", "y", undefined); + var a = regs[base + this.L(27, undefined, undefined)]; + regs[base + dst] = a * regs[base + this.L(null, [], null)]; + break; + } + case 65274: + { + var dst = this.L("k", 61, null); + var a = regs[base + this.L(84, null, {})]; + regs[base + dst] = a / regs[base + this.L(undefined, [], {})]; + break; + } + case 11271: + { + var dst = this.L(undefined, 27, {}); + var a = regs[base + this.L([], "q", null)]; + regs[base + dst] = a % regs[base + this.L("x", undefined, undefined)]; + break; + } + case 18230: + { + // Math.pow instead of `**` + var dst = this.L(80, 33, 73); + var a = regs[base + this.L(45, [], 41)]; + regs[base + dst] = Math.pow(a, regs[base + this.L([], undefined, [])]); + break; + } + case 21828: + { + var dst = this.L(null, 49, "z"); + var a = regs[base + this.L(undefined, [], {})]; + regs[base + dst] = a & regs[base + this.L("x", 35, "x")]; + break; + } + case 32609: + { + var dst = this.L(null, 78, null); + var a = regs[base + this.L(null, 35, undefined)]; + regs[base + dst] = a | regs[base + this.L("q", {}, {})]; + break; + } + case 15560: + { + var dst = this.L(null, {}, []); + var a = regs[base + this.L({}, {}, null)]; + regs[base + dst] = a ^ regs[base + this.L(undefined, undefined, "z")]; + break; + } + case 51471: + { + var dst = this.L(undefined, {}, undefined); + var a = regs[base + this.L(undefined, "q", {})]; + regs[base + dst] = a << regs[base + this.L({}, undefined, "z")]; + break; + } + case 50846: + { + var dst = this.L({}, undefined, {}); + var a = regs[base + this.L({}, undefined, [])]; + regs[base + dst] = a >> regs[base + this.L("k", "q", 47)]; + break; + } + case 44691: + { + var dst = this.L(9, {}, null); + var a = regs[base + this.L(null, undefined, {})]; + regs[base + dst] = a >>> regs[base + this.L(null, undefined, null)]; + break; + } + + // Comparison (dst, src1, src2) + case 11714: + { + var dst = this.L(62, undefined, 0); + var a = regs[base + this.L(9, 38, undefined)]; + regs[base + dst] = a < regs[base + this.L({}, {}, null)]; + break; + } + case 28433: + { + var dst = this.L(91, null, "k"); + var a = regs[base + this.L(61, [], "k")]; + regs[base + dst] = a > regs[base + this.L({}, [], undefined)]; + break; + } + case 4517: + { + var dst = this.L(null, undefined, undefined); + var a = regs[base + this.L([], undefined, undefined)]; + regs[base + dst] = a <= regs[base + this.L(undefined, [], 65)]; + break; + } + case 59488: + { + var dst = this.L("x", "q", null); + var a = regs[base + this.L(null, undefined, undefined)]; + regs[base + dst] = a >= regs[base + this.L({}, null, 72)]; + break; + } + case 64647: + { + var dst = this.L(undefined, 4, 52); + var a = regs[base + this.L({}, null, undefined)]; + regs[base + dst] = a === regs[base + this.L({}, undefined, {})]; + break; + } + case 13217: + { + var dst = this.L("q", "z", "q"); + var a = regs[base + this.L("k", null, "x")]; + regs[base + dst] = a !== regs[base + this.L({}, undefined, 12)]; + break; + } + case 45154: + { + var dst = this.L({}, [], {}); + var a = regs[base + this.L({}, 69, "k")]; + regs[base + dst] = a == regs[base + this.L({}, "q", "q")]; + break; + } + case 44273: + { + var dst = this.L(52, 27, {}); + var a = regs[base + this.L(null, undefined, null)]; + regs[base + dst] = a != regs[base + this.L([], null, null)]; + break; + } + case 23801: + { + var dst = this.L({}, [], 25); + var a = regs[base + this.L([], [], 96)]; + regs[base + dst] = a in regs[base + this.L("x", undefined, [])]; + break; + } + case 1968: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.L(null, "q", 63); + var obj = regs[base + this.L({}, undefined, "k")]; + regs[base + dst] = obj instanceof regs[base + this.L([], 72, null)]; + break; + } + + // Unary (dst, src) + case 23038: + { + var dst = this.L([], undefined, {}); + regs[base + dst] = -regs[base + this.L(undefined, [], "x")]; + break; + } + case 2315: + { + var dst = this.L(undefined, null, "x"); + regs[base + dst] = +regs[base + this.L({}, undefined, null)]; + break; + } + case 54482: + { + var dst = this.L(undefined, 22, undefined); + regs[base + dst] = !regs[base + this.L("x", {}, null)]; + break; + } + case 19322: + { + var dst = this.L(12, undefined, null); + regs[base + dst] = ~regs[base + this.L(53, {}, undefined)]; + break; + } + case 20202: + { + var dst = this.L(85, 49, null); + regs[base + dst] = typeof regs[base + this.L({}, "k", "k")]; + break; + } + case 49339: + { + var dst = this.L([], {}, undefined); + this.L("q", {}, {}); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 11916: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.L(37, 30, "z"); + var name = this.c(); + var val = Object.prototype.hasOwnProperty.call(this.d, name) ? this.d[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 7297: + regs[fp + 7] = this.L({}, "x", "q"); + break; + case 64871: + { + var src = this.L(undefined, [], "x"); + var target = this.L(undefined, undefined, []); + if (!regs[base + src]) regs[fp + 7] = target; + break; + } + case 17601: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.L(undefined, {}, "x"); + var target = this.L("k", undefined, 0); + if (regs[base + src]) regs[fp + 7] = target; + break; + } + + // Calls + case 39948: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.L("q", null, {}); + var callee = regs[base + this.L(undefined, "k", {})]; + var argc = this.L([], [], null); + var args; + if (argc === 1404279311) { + args = regs[base + this.L({}, undefined, "y")]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.L([], undefined, 3)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.I(dst << 1, args, {}, null, closure, this.d); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 27168: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.L([], "q", "z"); + var receiver = regs[base + this.L(undefined, 30, [])]; + var callee = regs[base + this.L(33, null, {})]; + var argc = this.L({}, [], {}); + var args; + if (argc === 1404279311) { + args = regs[base + this.L("q", {}, undefined)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.L(null, 57, "x")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.I(dst << 1, args, null, [], closure, receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 58650: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.L([], undefined, null); + var callee = regs[base + this.L("z", null, {})]; + var argc = this.L({}, null, []); + var args; + if (argc === 1404279311) { + args = regs[base + this.L("k", undefined, {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.L(25, "x", "y")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.I(dst << 1 | 1, args, [], {}, closure, Object.create(closure.prototype || null)); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 13431: + { + var retVal = regs[base + this.L(undefined, {}, [])]; + this.H(fp, 53); // must happen before frame is abandoned + + var caller = regs[fp + 2]; + var retDst = regs[fp + 1]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 4]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.u = fp; + this.o = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 0] + (retDst >> 1) : 0] = retVal; + break; + } + case 56258: + throw regs[base + this.L(undefined, null, [])]; + + // Closures + case 10642: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.L(undefined, "y", "k"); + var startPc = this.L(11, null, undefined); + var paramCount = this.L("x", undefined, "x"); + var regCount = this.L({}, undefined, []); + var uvCount = this.L("q", null, {}); + var hasRest = this.L([], [], {}); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.L({}, "q", {}); + var uvIndex = this.L([], 91, undefined); + uvDescs[i] = { + l: isLocalRaw, + D: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + X: paramCount, + C: regCount, + F: startPc, + z: hasRest + }; + var closure = new Closure(fn, []); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.l) { + closure.E.push(this.V([], undefined, fp, [], uvd.D)); + } else { + closure.E.push(regs[fp + 6].E[uvd.D]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.J, [], self.d, self.A).q(Array.prototype.slice.call(arguments), [], "k", c, this == null ? self.d : this, 82); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 49940: + { + // dst, count, [elemReg...] + var dst = this.L(24, undefined, {}); + var count = this.L({}, null, "q"); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.L("k", "q", null)]; + regs[base + dst] = elems; + break; + } + case 64668: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.L(null, null, 25); + var pairCount = this.L({}, {}, 2); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.L([], undefined, [])]; + var val = regs[base + this.L([], null, [])]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 24330: + { + // obj, key, fn + var obj = regs[base + this.L({}, [], null)]; + var key = regs[base + this.L([], null, undefined)]; + var getterFn = regs[base + this.L("y", "x", undefined)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 64651: + { + // obj, key, fn + var obj = regs[base + this.L(14, {}, [])]; + var key = regs[base + this.L(undefined, 75, [])]; + var setterFn = regs[base + this.L(46, 47, null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 65017: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.L(null, 71, []); + var obj = regs[base + this.L(null, {}, [])]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + P: keys, + S: 0 + }; + break; + } + case 10020: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.L({}, "q", {}); + var iter = regs[base + this.L("z", 63, 5)]; + var exitTarget = this.L({}, {}, "k"); + if (iter.S >= iter.P.length) { + regs[fp + 7] = exitTarget; + } else { + regs[base + dst] = iter.P[iter.S++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 19519: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 9]; + if (!hs) regs[fp + 9] = hs = []; + hs.push({ + R: this.L(undefined, {}, undefined), + b: this.L([], 72, 56) + }); + break; + } + case 13991: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 9].pop(); + break; + } + case 40589: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 9]; + if (!hs) regs[fp + 9] = hs = []; + hs.push({ + x: this.L(undefined, 66, 29), + M: this.L({}, [], 64), + K: this.L("q", 39, {}), + v: this.L(null, [], null) + }); + break; + } + + // Self-modifying bytecode + case 1001: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.L([], [], 2); + var sliceStart = this.L("z", {}, {}); + var sliceEnd = this.L(59, [], {}); + var pk = this.L([], null, undefined) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.J[destPc + (pi - sliceStart)] = (this.J[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 24210: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 7] = regs[base + this.L([], 90, "x")]; + break; + } + case 27223: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 7] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.o; + while (searchFrame) { + var handlers = regs[searchFrame + 9]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.H(searchFrame, 59); + this.u = searchFrame; + searchFrame = regs[searchFrame + 2]; + this.o = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 9].pop(); + var hBase = regs[handledFrame + 0]; + if (h.b !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.b] = err; + regs[handledFrame + 7] = h.R; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.M] = h.v; + regs[hBase + h.K] = err; + regs[handledFrame + 7] = h.x; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.u = handledFrame + regs[handledFrame + 4]; + this.o = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.o) return this.T[0]; + } + this.Q = fake_18; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), null, globals, CONSTANTS); +vm.q(null // no arguments object / parameter setup for the root frame +, {}, {}, new Closure({ + X: 0, + C: 7, + F: 0 +}, undefined), undefined, null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-183.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-183.js new file mode 100644 index 00000000..e2c2b50e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-183.js @@ -0,0 +1,958 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(absSlot, fake_4, fake_2, fake_3, fake_1, regs) { + this.e = fake_1; + this.p = regs; // shared reference to VM._regs flat array + this.n = fake_4; + this.r = absSlot; // absolute index; stable as long as frame is alive + this.g = false; + this.ab = fake_3; + this.y = fake_2; + this.ag = undefined; +} +// Closure & Frame +function Closure(fn, fake_11) { + this.z = fn; + this.ad = []; + this.prototype = {}; // <- default prototype object for `new` + this.h = fake_11; +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, fake_12, globals) { + this.b = bytecode; + this.j = constants; + this.f = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.k = fake_12; + this.U = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.p = []; + this.S = 7; + this.m = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +var BYTECODE = [63827, 1, 38527, 2, 0, 0, 1783, 3, 1, 0, 1783, 4, 2, 0, 1783, 5, 3, 0, 56749, 6, 4, 5, 1783, 4, 4, 0, 60230, 5, 6, 4, 1783, 4, 5, 0, 38326, 6, 5, 4, 42057, 2, 3, 6, 1783, 2, 6, 0, 4387, 2]; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CLOSURE_MAP = new WeakMap(); +VM.prototype.a = function (fake_16, fake_13, fake_14, fake_15) { + return this.b[this.p[this.m + 4]++]; + this.c = [fake_13, fake_16]; + this.J = [fake_15, fake_14]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.Q = function (retDst, fake_19, thisVal, fake_17, args, closure, fake_18) { + var fn = closure.z; + var regs = this.p; + var fp = this.S; + var size = 16 + fn.l; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 16; + regs[fp + 4] = fn.H; + regs[fp + 13] = this.m; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + this.af = fake_17 || fake_18; + regs[fp + 8] = closure; + regs[fp + 15] = size; + regs[fp + 10] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + this.L = fake_19; + if (typeof 12 === "number") regs[fp + 12] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.V; + if (typeof 7 === "number") regs[fp + 7] = args; + if (typeof 1 === "number") regs[fp + 1] = fn.H; + if (typeof 9 === "number") regs[fp + 9] = 0; + this.S = end; + if (args) { + if (fn.ae) { + var restSlot = fn.V - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.l; a++) regs[base + a] = args[a]; + } + if (fn.V < fn.l) regs[base + fn.V] = args; + } + this.m = fp; +}; +Upvalue.prototype.t = function (fake_7, fake_9, fake_10, fake_8) { + this.F = fake_10; + this.ag = this.p[this.r]; + this.g = true; + this.R = fake_7; + this.ac = fake_8; + this.D = fake_9; +}; +VM.prototype.P = function (fake_25, fake_26, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.U; + if (!uvs) return; // nothing in this program ever captured a local + this.T = fake_25 && fake_26; + var regs = this.p; + var hi = fp + regs[fp + 15]; + for (var i = regs[fp + 10]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.t("x", undefined, undefined, 53); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.G = function (fake_23, keyIn, fake_24, idxIn) { + var idx = idxIn ?? this.a(62, 27, null, "k"); + var key = keyIn ?? this.a({}, {}, "y", undefined); + var v = this.j[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + this.M = fake_24; + var out = ""; + this.E = fake_23; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +Upvalue.prototype.i = function (fake_5) { + return this.g ? this.ag : this.p[this.r]; + this.v = fake_5; +}; +VM.prototype.W = function (slot, fake_21, fake_20, fake_22, fp) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.p[fp + 10] + slot; + this.u = fake_21; + var uvs = this.U || (this.U = []); + this.Z = fake_22 && fake_20; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, 24, undefined, null, [], this.p)); +}; +Upvalue.prototype.B = function (v, fake_6) { + if (this.g) this.ag = v;else this.p[this.r] = v; + this.d = fake_6; +}; +VM.prototype.I = function (closure, fake_28, thisVal, fake_27, fake_29, fake_30, args) { + var now = () => { + return performance.now(); + }; + this.X = [fake_27, fake_29]; + this.K = fake_28; + var lastTime = now(); + this.Q(0, undefined, thisVal, [], args, closure, {}); + while (true) { + var fp = this.m; + var regs = this.p; + var bc = this.b; + var pc = regs[fp + 4]; + if (pc >= bc.length) break; + regs[fp + 4] = pc + 1; + var op = this.b[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof 1 === "number") regs[fp + 1] = (regs[fp + 1] + 1) % bc.length; + if (typeof 9 === "number") regs[fp + 9]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.b.length; i++) this.b[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.S; i2++) this.p[i2] = undefined; + op = 19772; + regs[fp + 4] = this.b.length; // jump past end to halt + } + } + try { + var base = regs[fp + 10]; + + /* @SWITCH */ + switch (op) { + case 1783: + { + var dst = this.a([], [], [], null); + regs[base + dst] = this.G(); + break; + } + case 36363: + { + var dst = this.a(undefined, "k", [], null); + regs[base + dst] = this.a(undefined, {}, null, {}); + break; + } + case 38527: + { + var dst = this.a([], "k", "z", {}); + var globalName = this.G(); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; + break; + } + case 41838: + { + var dst = this.a(undefined, [], null, null); + regs[base + dst] = regs[fp + 8].ad[this.a("y", "k", null, 93)].i(42); + break; + } + case 63827: + { + var dst = this.a(34, [], {}, []); + regs[base + dst] = regs[fp + 3]; + break; + } + case 25408: + { + var dst = this.a(undefined, {}, null, 4); + regs[base + dst] = regs[base + this.a([], null, {}, {})]; + break; + } + case 51321: + { + // globals[globalName] = regs[src] + this.f[this.G()] = regs[base + this.a({}, "k", {}, "y")]; + break; + } + case 53456: + { + var uvIdx = this.a("y", undefined, undefined, null); + regs[fp + 8].ad[uvIdx].B(regs[base + this.a({}, "q", null, [])], null); + break; + } + case 1227: + { + // dst = regs[obj][regs[key]] + var dst = this.a(null, [], undefined, null); + var obj = regs[base + this.a({}, "y", {}, "k")]; + var key = regs[base + this.a({}, [], "z", null)]; + regs[base + dst] = obj[key]; + break; + } + case 42057: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.a(83, {}, {}, null)]; + var key = regs[base + this.a([], [], [], 70)]; + var val = regs[base + this.a(undefined, undefined, undefined, null)]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 65260: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.a(undefined, "q", null, undefined); + var obj = regs[base + this.a(76, undefined, 41, 95)]; + var key = regs[base + this.a([], undefined, null, [])]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 60230: + { + var dst = this.a(undefined, {}, {}, []); + var a = regs[base + this.a({}, 19, undefined, "x")]; + regs[base + dst] = a + regs[base + this.a([], "k", undefined, {})]; + break; + } + case 3760: + { + var dst = this.a({}, 28, [], null); + var a = regs[base + this.a({}, {}, undefined, {})]; + regs[base + dst] = a - regs[base + this.a(undefined, {}, [], [])]; + break; + } + case 56749: + { + var dst = this.a({}, 37, 45, []); + var a = regs[base + this.a(null, undefined, "z", 43)]; + regs[base + dst] = a * regs[base + this.a(undefined, undefined, 5, [])]; + break; + } + case 35397: + { + var dst = this.a([], "y", 34, "z"); + var a = regs[base + this.a("k", 54, "z", undefined)]; + regs[base + dst] = a / regs[base + this.a(65, 43, undefined, "x")]; + break; + } + case 38326: + { + var dst = this.a(null, {}, {}, 56); + var a = regs[base + this.a(undefined, [], 89, undefined)]; + regs[base + dst] = a % regs[base + this.a(null, null, 91, {})]; + break; + } + case 52601: + { + // Math.pow instead of `**` + var dst = this.a("q", null, 3, 16); + var a = regs[base + this.a("q", "q", [], {})]; + regs[base + dst] = Math.pow(a, regs[base + this.a(43, {}, undefined, 79)]); + break; + } + case 7156: + { + var dst = this.a(null, "x", 43, {}); + var a = regs[base + this.a(null, 42, {}, null)]; + regs[base + dst] = a & regs[base + this.a("q", 14, "y", undefined)]; + break; + } + case 33721: + { + var dst = this.a(null, "k", null, []); + var a = regs[base + this.a(20, null, undefined, 75)]; + regs[base + dst] = a | regs[base + this.a(undefined, null, null, "y")]; + break; + } + case 40723: + { + var dst = this.a(undefined, null, null, []); + var a = regs[base + this.a("z", 84, null, undefined)]; + regs[base + dst] = a ^ regs[base + this.a("z", [], "y", null)]; + break; + } + case 46661: + { + var dst = this.a(null, "x", "y", undefined); + var a = regs[base + this.a({}, 39, {}, undefined)]; + regs[base + dst] = a << regs[base + this.a(undefined, undefined, {}, [])]; + break; + } + case 9322: + { + var dst = this.a(null, {}, undefined, []); + var a = regs[base + this.a({}, [], 20, {})]; + regs[base + dst] = a >> regs[base + this.a([], [], 2, null)]; + break; + } + case 64414: + { + var dst = this.a(62, "q", undefined, []); + var a = regs[base + this.a(null, 78, 36, {})]; + regs[base + dst] = a >>> regs[base + this.a(undefined, null, null, [])]; + break; + } + + // Comparison (dst, src1, src2) + case 41605: + { + var dst = this.a("x", null, 72, "q"); + var a = regs[base + this.a(undefined, undefined, null, "q")]; + regs[base + dst] = a < regs[base + this.a(undefined, undefined, "x", 64)]; + break; + } + case 5756: + { + var dst = this.a(null, null, {}, null); + var a = regs[base + this.a(undefined, "z", null, {})]; + regs[base + dst] = a > regs[base + this.a(undefined, [], undefined, {})]; + break; + } + case 40098: + { + var dst = this.a(null, [], [], 97); + var a = regs[base + this.a(undefined, undefined, "x", 56)]; + regs[base + dst] = a <= regs[base + this.a(71, undefined, "z", "y")]; + break; + } + case 171: + { + var dst = this.a(null, [], [], []); + var a = regs[base + this.a("z", undefined, "x", undefined)]; + regs[base + dst] = a >= regs[base + this.a("x", {}, {}, null)]; + break; + } + case 11047: + { + var dst = this.a(null, 58, "k", null); + var a = regs[base + this.a({}, undefined, null, {})]; + regs[base + dst] = a === regs[base + this.a([], {}, [], null)]; + break; + } + case 38434: + { + var dst = this.a(undefined, {}, 58, null); + var a = regs[base + this.a(null, undefined, {}, undefined)]; + regs[base + dst] = a !== regs[base + this.a(null, {}, "z", null)]; + break; + } + case 23794: + { + var dst = this.a(undefined, undefined, {}, null); + var a = regs[base + this.a(4, 80, "y", "k")]; + regs[base + dst] = a == regs[base + this.a("y", [], {}, [])]; + break; + } + case 52535: + { + var dst = this.a(null, [], null, null); + var a = regs[base + this.a(78, {}, [], undefined)]; + regs[base + dst] = a != regs[base + this.a(76, "y", null, "x")]; + break; + } + case 12233: + { + var dst = this.a(null, undefined, [], []); + var a = regs[base + this.a({}, "k", 47, undefined)]; + regs[base + dst] = a in regs[base + this.a(undefined, "q", "k", null)]; + break; + } + case 5881: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.a({}, {}, [], 56); + var obj = regs[base + this.a(55, {}, null, 1)]; + regs[base + dst] = obj instanceof regs[base + this.a(undefined, {}, {}, 98)]; + break; + } + + // Unary (dst, src) + case 23545: + { + var dst = this.a({}, {}, 88, {}); + regs[base + dst] = -regs[base + this.a({}, 78, undefined, null)]; + break; + } + case 42420: + { + var dst = this.a([], "z", undefined, 46); + regs[base + dst] = +regs[base + this.a(85, null, undefined, undefined)]; + break; + } + case 25347: + { + var dst = this.a({}, 70, null, "x"); + regs[base + dst] = !regs[base + this.a({}, {}, {}, null)]; + break; + } + case 7367: + { + var dst = this.a({}, "k", 74, []); + regs[base + dst] = ~regs[base + this.a([], undefined, [], {})]; + break; + } + case 42831: + { + var dst = this.a(8, {}, undefined, []); + regs[base + dst] = typeof regs[base + this.a(undefined, 51, [], [])]; + break; + } + case 41502: + { + var dst = this.a([], null, [], null); + this.a(80, "q", [], undefined); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 62003: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.a([], undefined, [], "y"); + var name = this.G(); + var val = Object.prototype.hasOwnProperty.call(this.f, name) ? this.f[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 19772: + regs[fp + 4] = this.a(undefined, null, 53, undefined); + break; + case 55595: + { + var src = this.a(null, undefined, "q", undefined); + var target = this.a("q", [], {}, {}); + if (!regs[base + src]) regs[fp + 4] = target; + break; + } + case 25181: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.a([], 30, "k", 23); + var target = this.a({}, [], {}, undefined); + if (regs[base + src]) regs[fp + 4] = target; + break; + } + + // Calls + case 45847: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.a("k", null, [], null); + var callee = regs[base + this.a(null, [], "k", {})]; + var argc = this.a([], [], [], null); + var args; + if (argc === 2617719534) { + args = regs[base + this.a(null, 94, "k", {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.a([], 39, null, {})]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.Q(dst << 1, [], this.f, 91, args, closure, "y"); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 6030: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.a({}, [], "k", 15); + var receiver = regs[base + this.a(null, "k", [], 45)]; + var callee = regs[base + this.a(null, null, "z", null)]; + var argc = this.a(80, [], null, undefined); + var args; + if (argc === 2617719534) { + args = regs[base + this.a(undefined, "z", "x", [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.a(null, undefined, 58, undefined)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.Q(dst << 1, 40, receiver, {}, args, closure, {}); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 51590: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.a({}, "k", {}, null); + var callee = regs[base + this.a(null, {}, [], 98)]; + var argc = this.a("z", "z", {}, {}); + var args; + if (argc === 2617719534) { + args = regs[base + this.a(58, "z", undefined, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.a(undefined, {}, "q", "x")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.Q(dst << 1 | 1, 70, Object.create(closure.prototype || null), null, args, closure, null); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 4387: + { + var retVal = regs[base + this.a(undefined, undefined, null, {})]; + this.P(96, "y", fp); // must happen before frame is abandoned + + var caller = regs[fp + 13]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 15]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.S = fp; + this.m = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 10] + (retDst >> 1) : 0] = retVal; + break; + } + case 6897: + throw regs[base + this.a(undefined, null, "y", {})]; + + // Closures + case 63522: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.a({}, [], "k", 56); + var startPc = this.a(null, 73, {}, {}); + var paramCount = this.a(48, 32, null, []); + var regCount = this.a(undefined, {}, 94, null); + var uvCount = this.a({}, undefined, [], undefined); + var hasRest = this.a(null, undefined, {}, "z"); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.a(null, {}, 55, 41); + var uvIndex = this.a([], null, [], null); + uvDescs[i] = { + s: isLocalRaw, + o: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + V: paramCount, + l: regCount, + H: startPc, + ae: hasRest + }; + var closure = new Closure(fn, 68); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.s) { + closure.ad.push(this.W(uvd.o, [], null, undefined, fp)); + } else { + closure.ad.push(regs[fp + 8].ad[uvd.o]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.b, self.j, null, self.f).I(c, undefined, this == null ? self.f : this, null, [], "y", Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 57913: + { + // dst, count, [elemReg...] + var dst = this.a(undefined, {}, "x", null); + var count = this.a(undefined, [], undefined, []); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.a({}, null, null, [])]; + regs[base + dst] = elems; + break; + } + case 51230: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.a(34, "x", null, null); + var pairCount = this.a([], null, "y", []); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.a(54, [], "y", 15)]; + var val = regs[base + this.a(undefined, 8, "x", null)]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50853: + { + // obj, key, fn + var obj = regs[base + this.a(40, {}, undefined, {})]; + var key = regs[base + this.a(89, null, "q", undefined)]; + var getterFn = regs[base + this.a([], "q", {}, {})]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 9477: + { + // obj, key, fn + var obj = regs[base + this.a({}, {}, "y", 10)]; + var key = regs[base + this.a("y", {}, {}, {})]; + var setterFn = regs[base + this.a({}, [], "k", "y")]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 48242: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.a(undefined, "y", [], {}); + var obj = regs[base + this.a({}, "q", null, {})]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + x: keys, + O: 0 + }; + break; + } + case 39882: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.a(undefined, [], {}, undefined); + var iter = regs[base + this.a(44, 1, [], [])]; + var exitTarget = this.a(4, [], [], "y"); + if (iter.O >= iter.x.length) { + regs[fp + 4] = exitTarget; + } else { + regs[base + dst] = iter.x[iter.O++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 60503: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + Y: this.a({}, "y", undefined, []), + aa: this.a(97, 10, [], []) + }); + break; + } + case 10682: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 34663: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + A: this.a({}, {}, 52, null), + C: this.a(95, [], 72, "q"), + q: this.a(6, [], "x", null), + N: this.a({}, {}, {}, null) + }); + break; + } + + // Self-modifying bytecode + case 19927: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.a("k", undefined, 79, undefined); + var sliceStart = this.a(null, {}, undefined, undefined); + var sliceEnd = this.a("y", 66, {}, {}); + var pk = this.a([], undefined, null, undefined) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.b[destPc + (pi - sliceStart)] = (this.b[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 41819: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 4] = regs[base + this.a({}, undefined, [], 97)]; + break; + } + case 49100: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 4] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.m; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.P("z", undefined, searchFrame); + this.S = searchFrame; + searchFrame = regs[searchFrame + 13]; + this.m = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 10]; + if (h.aa !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.aa] = err; + regs[handledFrame + 4] = h.Y; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.C] = h.N; + regs[hBase + h.q] = err; + regs[handledFrame + 4] = h.A; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.S = handledFrame + regs[handledFrame + 15]; + this.m = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.m) return this.p[0]; + } + this.w = fake_30; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, undefined, globals); +vm.I(new Closure({ + V: 0, + l: 7, + H: 0 +}, {}), null, undefined, null, undefined, [], null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-191.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-191.js new file mode 100644 index 00000000..28f9f5aa --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-191.js @@ -0,0 +1,18 @@ +function z(a,f,g){this.e=g;this.m=f;this.b=a;this.p=null;this.g=[];this.h=7;this.s=0}var A=new WeakMap;function B(a){this.F=a;this.x=[];this.prototype={}}function da(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})}function ea(a,f){this.g=a;this.j=f;this.l=!1;this.E=[];this.z=void 0} +function C(a,f,g,b,k){for(var e=b.F,l=a.g,m=a.h,c=11+e.i,d=m+c;l.length>>13)&65535);return f}function fa(a){a.r="q";a.k=void 0;return a.l?a.z:a.g[a.j]}function P(a,f){var g=a.p;if(g){var b=a.g;a=f+b[f+7];for(f=b[f+2];f=a.e.length)break;b[e+10]=l+1;var m=a.e[l];b[e+6]=b[e+6]^l|0;try{var c=b[e+2];switch(m){case 2404:var d=a.d(null);b[c+d]=I(a);break;case 47031:d=a.d([]);b[c+d]=a.d([]);break;case 37139:d=a.d(null);var p=I(a);if(!(p in a.b))throw new ReferenceError(`${p} is not defined`);b[c+d]=a.b[p];break;case 41872:d=a.d(void 0);b[c+d]=fa(b[e+1].x[a.d("k")]);break;case 36065:d=a.d(void 0);b[c+d]=b[e+8];break;case 20288:d=a.d(void 0); +b[c+d]=b[c+a.d({})];break;case 50365:a.b[I(a)]=b[c+a.d("k")];break;case 17862:var ja=a.d("k");ha(b[e+1].x[ja],b[c+a.d([])]);break;case 18366:d=a.d(void 0);var n=b[c+a.d(void 0)],q=b[c+a.d({})];b[c+d]=n[q];break;case 15022:n=b[c+a.d(84)];q=b[c+a.d(void 0)];var w=b[c+a.d({})];Reflect.set(n,q,w);break;case 32124:d=a.d(void 0);n=b[c+a.d([])];q=b[c+a.d("z")];b[c+d]=delete n[q];break;case 3623:d=a.d("z");var h=b[c+a.d(73)];b[c+d]=h+b[c+a.d([])];break;case 10183:d=a.d(81);h=b[c+a.d(void 0)];b[c+d]=h-b[c+ +a.d({})];break;case 7357:d=a.d([]);h=b[c+a.d("x")];b[c+d]=h*b[c+a.d({})];break;case 54961:d=a.d("z");h=b[c+a.d(void 0)];b[c+d]=h/b[c+a.d("x")];break;case 21305:d=a.d(62);h=b[c+a.d(49)];b[c+d]=h%b[c+a.d("z")];break;case 20159:d=a.d(void 0);h=b[c+a.d({})];b[c+d]=Math.pow(h,b[c+a.d("k")]);break;case 13292:d=a.d(null);h=b[c+a.d({})];b[c+d]=h&b[c+a.d({})];break;case 57867:d=a.d(void 0);h=b[c+a.d(24)];b[c+d]=h|b[c+a.d([])];break;case 60028:d=a.d(null);h=b[c+a.d(null)];b[c+d]=h^b[c+a.d(76)];break;case 16709:d= +a.d(10);h=b[c+a.d([])];b[c+d]=h<>b[c+a.d(void 0)];break;case 56840:d=a.d(85);h=b[c+a.d({})];b[c+d]=h>>>b[c+a.d(68)];break;case 18329:d=a.d(37);h=b[c+a.d({})];b[c+d]=hb[c+a.d(15)];break;case 43634:d=a.d([]);h=b[c+a.d(void 0)];b[c+d]=h<=b[c+a.d(54)];break;case 17626:d=a.d(void 0);h=b[c+a.d([])];b[c+d]=h>=b[c+a.d(void 0)];break;case 49949:d=a.d(void 0);h=b[c+a.d(null)]; +b[c+d]=h===b[c+a.d("k")];break;case 5541:d=a.d({});h=b[c+a.d(void 0)];b[c+d]=h!==b[c+a.d(void 0)];break;case 24760:d=a.d([]);h=b[c+a.d("q")];b[c+d]=h==b[c+a.d(void 0)];break;case 39806:d=a.d(null);h=b[c+a.d({})];b[c+d]=h!=b[c+a.d(51)];break;case 9427:d=a.d(void 0);h=b[c+a.d(void 0)];b[c+d]=h in b[c+a.d("x")];break;case 38412:d=a.d(50);n=b[c+a.d(void 0)];b[c+d]=n instanceof b[c+a.d(null)];break;case 24086:d=a.d("k");b[c+d]=-b[c+a.d(84)];break;case 14554:d=a.d(void 0);b[c+d]=+b[c+a.d(void 0)];break; +case 2552:d=a.d(null);b[c+d]=!b[c+a.d([])];break;case 23691:d=a.d(void 0);b[c+d]=~b[c+a.d("k")];break;case 48768:d=a.d("k");b[c+d]=typeof b[c+a.d({})];break;case 19131:d=a.d({});a.d([]);b[c+d]=void 0;break;case 64685:d=a.d(51);var S=I(a);w=Object.prototype.hasOwnProperty.call(a.b,S)?a.b[S]:void 0;b[c+d]=typeof w;break;case 53096:b[e+10]=a.d("y");break;case 49894:var J=a.d([]),K=a.d(0);b[c+J]||(b[e+10]=K);break;case 21205:J=a.d([]);K=a.d(26);b[c+J]&&(b[e+10]=K);break;case 55437:d=a.d(7);var r=b[c+ +a.d(void 0)],t=a.d(void 0);if(t===1265226110)g=b[c+a.d(void 0)];else for(g=Array(t),k=0;k>1):0]=D;break;case 52724:throw b[c+a.d([])];case 35774:d=a.d("y");var la=a.d("z"),ma=a.d(null),na=a.d({}),V=a.d(null),oa=a.d([]),M=Array(V);for(k=0;k=G.f.length?b[e+10]=ua:b[c+d]=G.f[G.o++];break;case 43507:var v=b[e+4];v||(b[e+4]=v=[]);v.push({v:a.d([]),t:a.d("q")});break;case 25651:b[e+4].pop();break;case 19248:(v=b[e+4])||(b[e+4]= +v=[]);v.push({B:a.d(null),n:a.d({}),Q:a.d(null),A:a.d(void 0)});break;case 46991:var ba=a.d({}),ca=a.d(null),va=a.d("k"),H=a.d([])^ba|0;for(e=ca;e>>13)>>>0;break;case 62264:b[e+10]=b[c+a.d(48)];break;case 35145:debugger;break;default:throw Error("Unknown opcode: "+m+" at pc "+(b[e+10]-1));}}catch(x){e=0;for(l=a.s;l;){if((m=b[l+4])&&m.length>0){e=l;break}P(a,l);a.h=l;l=b[l+5];a.s=l}if(!e)throw x;l=b[e+4].pop();m=b[e+2];l.t!==void 0?(b[m+l.t]=x,b[e+ +10]=l.v):(b[m+l.n]=l.A,b[m+l.Q]=x,b[e+10]=l.B);a.h=e+b[e+7];a.s=e}if(!a.s)return a.g[0]}}var R=globalThis;typeof window!=="undefined"&&(R.window=window,R.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(R.module=module,R.exports=typeof exports!=="undefined"?exports:void 0); +Q(new z(R,["window","TEST_OUTPUT",6,7,3,5,void 0],[36065,1,37139,2,0,0,2404,3,1,0,2404,4,2,0,2404,5,3,0,7357,6,4,5,2404,4,4,0,3623,5,6,4,2404,4,5,0,21305,6,5,4,15022,2,3,6,2404,2,6,0,54001,2]),new B({c:0,i:7,u:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-192.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-192.js new file mode 100644 index 00000000..a8285880 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-192.js @@ -0,0 +1,19 @@ +function x(a){this.G=a;this.l=[];this.prototype={}}function B(a,f,g,b,e){this.i=e;this.D=f;this.k=g;this.u=null;this.g=[];this.a=[a,b];this.j=2;this.h=0}function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})}var C=new WeakMap;function D(a,f){this.g=f;this.o=a;this.q=!1;this.v=void 0} +function E(a,f,g,b,e,k){for(var l=e.G,m=a.g,c=a.j,d=10+l.m,u=c+d;m.length>>13)&65535);return f}function fa(a){a.c=void 0;return a.q?a.v:a.g[a.o]}B.prototype.p=function(){return this.i[this.g[this.h+8]++]};B.prototype.z=function(a,f,g,b,e,k){a=this.g[k+7]+f;f=this.u||(this.u=[]);return f[a]||(f[a]=new D(a,this.g))}; +D.prototype.s=function(a,f){this.q?this.v=f:this.g[this.o]=f;this.w=a};B.prototype.y=function(a,f,g,b){if(f=this.u){var e=this.g;this.f=b;b=g+e[g+9];this.n=a;for(a=e[g+7];a=l.length)break;b[e+8]=m+1;m=a.i[m];b[e+5]=(b[e+5]+1)%l.length;try{var c=b[e+7];switch(m){case 5311:var d=a.p({},null);b[c+d]=Q(a);break;case 29785:d=a.p(void 0,{});b[c+d]=a.p([],14);break;case 41170:d=a.p(89,"x");var u=Q(a);if(!(u in a.k))throw new ReferenceError(`${u} is not defined`);b[c+d]=a.k[u];break;case 56573:d=a.p("q",null);b[c+d]=fa(b[e+4].l[a.p("y",null)]);break;case 56670:d=a.p([],void 0);b[c+ +d]=b[e+2];break;case 44637:d=a.p(42,"y");b[c+d]=b[c+a.p({},void 0)];break;case 20843:a.k[Q(a)]=b[c+a.p({},[])];break;case 2347:var q=a.p("y",{});b[e+4].l[q].s(19,b[c+a.p([],[])]);break;case 53667:d=a.p(37,49);var n=b[c+a.p({},"x")],p=b[c+a.p(null,[])];b[c+d]=n[p];break;case 54165:n=b[c+a.p(void 0,void 0)];p=b[c+a.p(null,[])];var y=b[c+a.p([],{})];Reflect.set(n,p,y);break;case 10406:d=a.p(null,65);n=b[c+a.p([],void 0)];p=b[c+a.p(43,[])];b[c+d]=delete n[p];break;case 27840:d=a.p({},"z");var h=b[c+a.p(null, +{})];b[c+d]=h+b[c+a.p(void 0,"q")];break;case 934:d=a.p(void 0,"z");h=b[c+a.p(null,[])];b[c+d]=h-b[c+a.p("q","q")];break;case 22512:d=a.p("z",82);h=b[c+a.p(74,[])];b[c+d]=h*b[c+a.p("k",{})];break;case 38776:d=a.p([],[]);h=b[c+a.p("y","k")];b[c+d]=h/b[c+a.p(0,{})];break;case 16591:d=a.p({},null);h=b[c+a.p(void 0,{})];b[c+d]=h%b[c+a.p([],void 0)];break;case 22715:d=a.p([],null);h=b[c+a.p(66,null)];b[c+d]=Math.pow(h,b[c+a.p([],null)]);break;case 40046:d=a.p(null,[]);h=b[c+a.p("x","x")];b[c+d]=h&b[c+ +a.p(32,void 0)];break;case 16840:d=a.p({},void 0);h=b[c+a.p(72,[])];b[c+d]=h|b[c+a.p(54,80)];break;case 35019:d=a.p(63,[]);h=b[c+a.p({},[])];b[c+d]=h^b[c+a.p(void 0,{})];break;case 12158:d=a.p(null,[]);h=b[c+a.p("z",[])];b[c+d]=h<>b[c+a.p(void 0,void 0)];break;case 63201:d=a.p(null,[]);h=b[c+a.p(null,[])];b[c+d]=h>>>b[c+a.p({},5)];break;case 35824:d=a.p([],null);h=b[c+a.p([],{})];b[c+d]=hb[c+a.p(void 0,{})];break;case 44681:d=a.p(18,74);h=b[c+a.p({},null)];b[c+d]=h<=b[c+a.p(void 0,void 0)];break;case 41007:d=a.p(null,void 0);h=b[c+a.p({},"x")];b[c+d]=h>=b[c+a.p({},null)];break;case 34701:d=a.p(void 0,[]);h=b[c+a.p(null,null)];b[c+d]=h===b[c+a.p(void 0,void 0)];break;case 53970:d=a.p(32,null);h=b[c+a.p([],{})];b[c+d]=h!==b[c+a.p("k",[])];break;case 28535:d=a.p({},"y");h=b[c+a.p(73,{})];b[c+d]=h==b[c+a.p("z",{})];break;case 35900:d=a.p(86, +"k");h=b[c+a.p("y","z")];b[c+d]=h!=b[c+a.p(null,"k")];break;case 64062:d=a.p(null,22);h=b[c+a.p(void 0,{})];b[c+d]=h in b[c+a.p(87,"z")];break;case 10154:d=a.p("q",[]);n=b[c+a.p({},null)];b[c+d]=n instanceof b[c+a.p(void 0,"k")];break;case 54307:d=a.p(null,"z");b[c+d]=-b[c+a.p({},void 0)];break;case 13754:d=a.p({},4);b[c+d]=+b[c+a.p([],"q")];break;case 29330:d=a.p({},void 0);b[c+d]=!b[c+a.p("y","z")];break;case 54688:d=a.p(void 0,83);b[c+d]=~b[c+a.p("y",{})];break;case 63267:d=a.p(null,"q");b[c+d]= +typeof b[c+a.p(void 0,[])];break;case 38546:d=a.p([],45);a.p("k",null);b[c+d]=void 0;break;case 62836:d=a.p(void 0,78);var T=Q(a);y=Object.prototype.hasOwnProperty.call(a.k,T)?a.k[T]:void 0;b[c+d]=typeof y;break;case 60656:b[e+8]=a.p(5,null);break;case 22172:var K=a.p(null,[]),L=a.p(null,[]);b[c+K]||(b[e+8]=L);break;case 31454:K=a.p([],"z");L=a.p({},[]);b[c+K]&&(b[e+8]=L);break;case 33160:d=a.p([],void 0);var r=b[c+a.p(62,void 0)],t=a.p({},void 0);if(t===3208792843)g=b[c+a.p({},11)];else for(g=Array(t), +k=0;k>1):0]=F;break;case 42135:throw b[c+a.p(null,{})];case 1894:d=a.p({},"q");var ia=a.p({},{}),ja=a.p({},null),ka=a.p(null,void 0),W=a.p({},27),la=a.p(33,43),N=Array(W);for(k=0;k=I.B.length?b[e+8]=ra:b[c+d]=I.B[I.e++];break; +case 60149:var w=b[e+0];w||(b[e+0]=w=[]);w.push({Q:a.p("x",{}),C:a.p(null,null)});break;case 54011:b[e+0].pop();break;case 34588:(w=b[e+0])||(b[e+0]=w=[]);w.push({H:a.p(null,{}),I:a.p("x",{}),J:a.p("x",73),F:a.p({},[])});break;case 11569:var ca=a.p("y","z"),da=a.p([],[]),sa=a.p(null,"z"),J=a.p("z",void 0)^ca|0;for(e=da;e>>13)>>>0;break;case 36150:b[e+8]=b[c+a.p("q",73)];break;case 15003:debugger;break;default:throw Error("Unknown opcode: "+m+" at pc "+ +(b[e+8]-1));}}catch(z){e=0;for(l=a.h;l;){if((m=b[l+0])&&m.length>0){e=l;break}a.y("q",{},l,42);a.j=l;l=b[l+3];a.h=l}if(!e)throw z;l=b[e+0].pop();m=b[e+7];l.C!==void 0?(b[m+l.C]=z,b[e+8]=l.Q):(b[m+l.I]=l.F,b[m+l.J]=z,b[e+8]=l.H);a.j=e+b[e+9];a.h=e}if(!a.h)return a.g[0]}}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new B(null,["window","TEST_OUTPUT",6,7,3,5,void 0],S,{},[56670,1,41170,2,0,0,5311,3,1,0,5311,4,2,0,5311,5,3,0,22512,6,4,5,5311,4,4,0,27840,5,6,4,5311,4,5,0,16591,6,5,4,54165,2,3,6,5311,2,6,0,33142,2]),new x({d:0,m:7,A:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-193.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-193.js new file mode 100644 index 00000000..442d0559 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic-minified-193.js @@ -0,0 +1,19 @@ +function w(e,a){this.r=a;this.I=e;this.w=[];this.prototype={}}function A(e,a){this.g=a;this.v=e;this.o=!1;this.u=void 0}function da(e){return typeof Buffer!=="undefined"?Buffer.from(e,"base64"):Uint8Array.from(atob(e),function(a){return a.charCodeAt(0)})}var B=new WeakMap;function C(e,a,g,d){this.j=d;this.F=g;this.t=a;this.i=e;this.A=null;this.g=[];this.l=6;this.h=0}C.prototype.c=function(e,a,g,d){e=this.g[d+6]+a;a=this.A||(this.A=[]);this.y=g;return a[e]||(a[e]=new A(e,this.g))}; +A.prototype.x=function(e,a,g){this.f=g;this.o?this.u=e:this.g[this.v]=e};C.prototype.d=function(){return this.j[this.g[this.h+2]++]};function D(e){var a=e.d([]),g=e.d(12);e=e.F[a];if(!g)return e;if(typeof e==="number")return e^g;if(typeof e!=="string")return e;e=da(e);a="";for(var d=0;d>>13)&65535);return a}function ea(e){return e.o?e.u:e.g[e.v]} +function P(e,a,g,d,f,k,b){for(var c=d.I,l=e.g,p=e.l,m=15+c.m,n=p+m;l.length=this.j.length)break;a[d+2]=k+1;k=this.j[k];a[d+7]++;try{var b=a[d+6];switch(k){case 48173:var c=this.d([]);a[b+c]=D(this);break;case 165:c=this.d(null);a[b+c]=this.d({});break;case 40683:c=this.d({});var l=D(this);if(!(l in this.i))throw new ReferenceError(`${l} is not defined`);a[b+c]=this.i[l];break;case 64726:c=this.d("z");a[b+c]=ea(a[d+11].w[this.d([])]);break;case 32591:c=this.d(null); +a[b+c]=a[d+0];break;case 54717:c=this.d(48);a[b+c]=a[b+this.d({})];break;case 4141:this.i[D(this)]=a[b+this.d(null)];break;case 54656:var p=this.d({});a[d+11].w[p].x(a[b+this.d(22)],{},44,[]);break;case 20296:c=this.d({});var m=a[b+this.d(void 0)],n=a[b+this.d(void 0)];a[b+c]=m[n];break;case 40621:m=a[b+this.d(51)];n=a[b+this.d(void 0)];var x=a[b+this.d(void 0)];Reflect.set(m,n,x);break;case 728:c=this.d(89);m=a[b+this.d(void 0)];n=a[b+this.d({})];a[b+c]=delete m[n];break;case 12927:c=this.d(void 0); +var h=a[b+this.d({})];a[b+c]=h+a[b+this.d([])];break;case 54142:c=this.d(27);h=a[b+this.d(86)];a[b+c]=h-a[b+this.d({})];break;case 28617:c=this.d([]);h=a[b+this.d("y")];a[b+c]=h*a[b+this.d([])];break;case 13717:c=this.d(null);h=a[b+this.d([])];a[b+c]=h/a[b+this.d(54)];break;case 12907:c=this.d(92);h=a[b+this.d({})];a[b+c]=h%a[b+this.d("z")];break;case 20742:c=this.d("q");h=a[b+this.d("k")];a[b+c]=Math.pow(h,a[b+this.d([])]);break;case 23382:c=this.d({});h=a[b+this.d("x")];a[b+c]=h&a[b+this.d({})]; +break;case 55577:c=this.d(51);h=a[b+this.d("y")];a[b+c]=h|a[b+this.d(void 0)];break;case 47611:c=this.d([]);h=a[b+this.d([])];a[b+c]=h^a[b+this.d(void 0)];break;case 32957:c=this.d("x");h=a[b+this.d(74)];a[b+c]=h<>a[b+this.d(void 0)];break;case 63987:c=this.d(void 0);h=a[b+this.d(79)];a[b+c]=h>>>a[b+this.d(null)];break;case 34619:c=this.d("y");h=a[b+this.d([])];a[b+c]=ha[b+this.d(80)];break;case 46904:c=this.d(49);h=a[b+this.d([])];a[b+c]=h<=a[b+this.d(26)];break;case 41281:c=this.d([]);h=a[b+this.d({})];a[b+c]=h>=a[b+this.d({})];break;case 28815:c=this.d(null);h=a[b+this.d({})];a[b+c]=h===a[b+this.d(void 0)];break;case 48755:c=this.d(void 0);h=a[b+this.d("q")];a[b+c]=h!==a[b+this.d(null)];break;case 63420:c=this.d({});h=a[b+this.d(null)];a[b+c]=h==a[b+this.d("k")];break;case 55099:c=this.d(void 0);h=a[b+this.d(null)];a[b+c]=h!=a[b+this.d([])]; +break;case 25677:c=this.d("q");h=a[b+this.d([])];a[b+c]=h in a[b+this.d(26)];break;case 46221:c=this.d([]);m=a[b+this.d("k")];a[b+c]=m instanceof a[b+this.d([])];break;case 60250:c=this.d(null);a[b+c]=-a[b+this.d([])];break;case 46577:c=this.d("z");a[b+c]=+a[b+this.d(null)];break;case 6263:c=this.d(void 0);a[b+c]=!a[b+this.d(44)];break;case 64253:c=this.d(null);a[b+c]=~a[b+this.d([])];break;case 43251:c=this.d([]);a[b+c]=typeof a[b+this.d("k")];break;case 20308:c=this.d([]);this.d([]);a[b+c]=void 0; +break;case 5179:c=this.d("y");var S=D(this);x=Object.prototype.hasOwnProperty.call(this.i,S)?this.i[S]:void 0;a[b+c]=typeof x;break;case 16135:a[d+2]=this.d("y");break;case 26982:var J=this.d([]),K=this.d([]);a[b+J]||(a[d+2]=K);break;case 36219:J=this.d(null);K=this.d({});a[b+J]&&(a[d+2]=K);break;case 63:c=this.d([]);var q=a[b+this.d({})],r=this.d("z");if(r===1330407607)g=a[b+this.d(83)];else for(g=Array(r),f=0;f>1):0]=E;break;case 25297:throw a[b+this.d([])];case 35054:c=this.d([]);var ha=this.d("x"),ia=this.d("y"),ja=this.d("q"),V=this.d({}),ka=this.d(null),M=Array(V);for(f=0;f=H.z.length?a[d+2]=qa:a[b+c]=H.z[H.b++];break;case 48497:var u=a[d+3];u||(a[d+3]=u=[]);u.push({H:this.d(null),C:this.d({})});break;case 5166:a[d+3].pop(); +break;case 3456:(u=a[d+3])||(a[d+3]=u=[]);u.push({K:this.d([]),E:this.d("k"),J:this.d([]),M:this.d(92)});break;case 27348:var ba=this.d({}),ca=this.d(void 0),ra=this.d(null),I=this.d({})^ba|0;for(d=ca;d>>13)>>>0;break;case 24842:a[d+2]=a[b+this.d({})];break;case 31048:debugger;break;default:throw Error("Unknown opcode: "+k+" at pc "+(a[d+2]-1));}}catch(y){d=0;for(k=this.h;k;){var v=a[k+3];if(v&&v.length>0){d=k;break}Q(this,k,{});this.l=k;this.h= +k=a[k+12]}if(!d)throw y;k=a[d+3].pop();v=a[d+6];k.C!==void 0?(a[v+k.C]=y,a[d+2]=k.H):(a[v+k.E]=k.M,a[v+k.J]=y,a[d+2]=k.K);this.l=d+a[d+8];this.h=d}if(!this.h)return this.g[0]}};var R=globalThis;typeof window!=="undefined"&&(R.window=window,R.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(R.module=module,R.exports=typeof exports!=="undefined"?exports:void 0); +(new C(R,[],["window","TEST_OUTPUT",6,7,3,5,void 0],[32591,1,40683,2,0,0,48173,3,1,0,48173,4,2,0,48173,5,3,0,28617,6,4,5,48173,4,4,0,12927,5,6,4,48173,4,5,0,12907,6,5,4,40621,2,3,6,48173,2,6,0,39588,2])).s(new w({Q:0,m:7,D:0},"z"),void 0,null,"q"); diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic.baseline.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic.baseline.js new file mode 100644 index 00000000..67c678fb --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic.baseline.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */6, /* 3 */7, /* 4 */3, /* 5 */5, /* 6 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 0, 0, 3, 1, 0, 0, 4, 2, 0, 0, 5, 3, 0, 13, 6, 4, 5, 0, 4, 4, 0, 11, 5, 6, 4, 0, 4, 5, 0, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic.source.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic.source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/arithmetic.source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-181.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-181.js new file mode 100644 index 00000000..7c9388d3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-181.js @@ -0,0 +1,960 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(fake_14, bytecode, constants, fake_13, fake_15, globals) { + this.v = bytecode; + this.X = constants; + this.ac = fake_14; + this.Y = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.N = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.f = []; + this.R = 11; + this.F = fake_15 || fake_13; + this.P = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +function Upvalue(absSlot, fake_3, regs, fake_2, fake_1) { + this.n = fake_3; + this.f = regs; // shared reference to VM._regs flat array + this.r = fake_2; + this.i = absSlot; // absolute index; stable as long as frame is alive + this.J = false; + this.o = undefined; + this.M = fake_1; +} +var BYTECODE = [2151, 1, 18527, 5, 89, 0, 4, 1, 0, 1, 2, 21164, 3, 5, 28412, 5, 0, 21164, 2, 5, 4545, 5, 3, 0, 21164, 4, 5, 56464, 5, 0, 0, 26188, 6, 1, 0, 26188, 7, 2, 0, 4545, 8, 4, 1, 7, 26188, 7, 3, 0, 4545, 9, 4, 1, 7, 26188, 7, 4, 0, 4545, 10, 4, 1, 7, 26188, 7, 5, 0, 4545, 11, 4, 1, 7, 28412, 7, 5, 8, 9, 10, 11, 2, 45975, 5, 6, 7, 26188, 5, 6, 0, 62456, 5, 2151, 1, 26188, 3, 2, 0, 21164, 2, 3, 18527, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 62456, 3, 26188, 3, 6, 0, 62456, 3, 2151, 2, 57952, 326, 4, 5, 353, 50691, 264, 3, 34526, 7, 0, 26188, 8, 7, 0, 63110, 9, 7, 8, 26188, 8, 8, 0, 37279, 10, 8, 0, 15913, 8, 7, 9, 1, 10, 26188, 7, 2, 0, 25819, 8, 0, 7, 37081, 8, 185, 56464, 7, 9, 0, 26188, 8, 10, 0, 63110, 9, 7, 8, 26188, 8, 11, 0, 15913, 10, 7, 9, 1, 8, 26188, 7, 3, 0, 25819, 8, 0, 7, 37081, 8, 211, 26188, 7, 12, 0, 21164, 6, 7, 26188, 7, 13, 0, 63110, 8, 6, 7, 26188, 7, 4, 0, 25819, 8, 0, 7, 37081, 8, 237, 56464, 7, 14, 0, 26188, 8, 15, 0, 52616, 9, 7, 1, 8, 19563, 9, 34526, 7, 1, 26188, 8, 3, 0, 37279, 9, 7, 8, 43086, 1, 9, 40557, 21164, 5, 9, 35667, 4, 355, 40557, 39919, 326, 40557, 39919, 320, 34526, 7, 0, 26188, 8, 7, 0, 63110, 9, 7, 8, 26188, 8, 16, 0, 26188, 10, 17, 0, 63110, 11, 3, 10, 37279, 10, 8, 11, 15913, 8, 7, 9, 1, 10, 34526, 7, 1, 26188, 8, 3, 0, 37279, 9, 7, 8, 43086, 1, 9, 21164, 5, 9, 35667, 4, 357, 40557, 39919, 326, 40557, 35667, 4, 359, 39919, 326, 34526, 7, 0, 26188, 8, 7, 0, 63110, 9, 7, 8, 26188, 8, 18, 0, 37279, 10, 8, 0, 15913, 8, 7, 9, 1, 10, 13119, 4, 19563, 5, 62456, 5, 62456, 5, 26188, 7, 6, 0, 62456, 7]; +// Closure & Frame +function Closure(fake_12, fake_11, fn) { + this.l = fn; + this.K = fake_12; + this.C = []; + this.prototype = {}; // <- default prototype object for `new` + this.s = fake_11; +} +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +VM.prototype.aa = function (fake_20, fp, fake_23, fake_22, slot, fake_21) { + this.p = fake_21 || fake_22; + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.f[fp + 7] + slot; + var uvs = this.N || (this.N = []); + this.G = fake_23; + this.Z = fake_20; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, "y", this.f, {}, [])); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.E = function (idxIn, fake_25, fake_24, keyIn) { + this.w = [fake_24, fake_25]; + var idx = idxIn ?? this.S(undefined); + var key = keyIn ?? this.S({}); + var v = this.X[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +Upvalue.prototype.O = function (fake_6, v, fake_7) { + if (this.J) this.o = v;else this.f[this.i] = v; + this.j = fake_7; + this.H = fake_6; +}; +Upvalue.prototype.u = function (fake_10, fake_8, fake_9) { + this.af = fake_10; + this.o = this.f[this.i]; + this.J = true; + this.h = fake_9 || fake_8; +}; +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.ag = function (fake_17, fake_18, fake_19, thisVal, retDst, args, closure) { + var fn = closure.l; + var regs = this.f; + this.W = fake_17; + var fp = this.R; + var size = 13 + fn.q; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 13; + regs[fp + 1] = fn.ab; + regs[fp + 10] = this.P; + this.e = fake_18; + regs[fp + 8] = retDst; + regs[fp + 2] = thisVal; + regs[fp + 3] = closure; + regs[fp + 4] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof 9 === "number") regs[fp + 9] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.U; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.ab; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.d = fake_19; + this.R = end; + if (args) { + if (fn.z) { + var restSlot = fn.U - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.q; a++) regs[base + a] = args[a]; + } + if (fn.U < fn.q) regs[base + fn.U] = args; + } + this.P = fp; +}; +VM.prototype.S = function (fake_16) { + this.k = fake_16; + return this.v[this.f[this.P + 1]++]; +}; +VM.prototype.b = function (fake_27, fake_28, fake_29, fake_26, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.N; + this.Q = fake_27; + if (!uvs) return; // nothing in this program ever captured a local + this.x = fake_29 && fake_26; + var regs = this.f; + this.g = fake_28; + var hi = fp + regs[fp + 4]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.u("q", null, undefined); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.ah = function (fake_4, fake_5) { + this.T = [fake_5, fake_4]; + return this.J ? this.o : this.f[this.i]; +}; +VM.prototype.t = function (closure, args, fake_31, thisVal, fake_30, fake_32) { + this.A = fake_30 || fake_31; + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.ag(null, null, [], thisVal, 0, args, closure); + this.a = fake_32; + while (true) { + var fp = this.P; + var regs = this.f; + var bc = this.v; + var pc = regs[fp + 1]; + if (pc >= bc.length) break; + regs[fp + 1] = pc + 1; + var op = this.v[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof 0 === "number") regs[fp + 0] = regs[fp + 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.v.length; i++) this.v[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.R; i2++) this.f[i2] = undefined; + op = 39919; + regs[fp + 1] = this.v.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 26188: + { + var dst = this.S([]); + regs[base + dst] = this.E(); + break; + } + case 35667: + { + var dst = this.S(74); + regs[base + dst] = this.S([]); + break; + } + case 56464: + { + var dst = this.S("q"); + var globalName = this.E(); + if (!(globalName in this.Y)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.Y[globalName]; + break; + } + case 34526: + { + var dst = this.S("y"); + regs[base + dst] = regs[fp + 3].C[this.S({})].ah(null, undefined); + break; + } + case 2151: + { + var dst = this.S(93); + regs[base + dst] = regs[fp + 2]; + break; + } + case 21164: + { + var dst = this.S({}); + regs[base + dst] = regs[base + this.S({})]; + break; + } + case 4576: + { + // globals[globalName] = regs[src] + this.Y[this.E()] = regs[base + this.S(undefined)]; + break; + } + case 43086: + { + var uvIdx = this.S({}); + regs[fp + 3].C[uvIdx].O(null, regs[base + this.S(67)], "y"); + break; + } + case 63110: + { + // dst = regs[obj][regs[key]] + var dst = this.S(null); + var obj = regs[base + this.S({})]; + var key = regs[base + this.S(75)]; + regs[base + dst] = obj[key]; + break; + } + case 45975: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.S({})]; + var key = regs[base + this.S(23)]; + var val = regs[base + this.S("k")]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 59697: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.S(null); + var obj = regs[base + this.S(43)]; + var key = regs[base + this.S("z")]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 37279: + { + var dst = this.S(undefined); + var a = regs[base + this.S({})]; + regs[base + dst] = a + regs[base + this.S([])]; + break; + } + case 363: + { + var dst = this.S(undefined); + var a = regs[base + this.S([])]; + regs[base + dst] = a - regs[base + this.S({})]; + break; + } + case 64040: + { + var dst = this.S([]); + var a = regs[base + this.S(22)]; + regs[base + dst] = a * regs[base + this.S(null)]; + break; + } + case 64597: + { + var dst = this.S(undefined); + var a = regs[base + this.S([])]; + regs[base + dst] = a / regs[base + this.S([])]; + break; + } + case 64807: + { + var dst = this.S(undefined); + var a = regs[base + this.S({})]; + regs[base + dst] = a % regs[base + this.S("k")]; + break; + } + case 30314: + { + // Math.pow instead of `**` + var dst = this.S("q"); + var a = regs[base + this.S({})]; + regs[base + dst] = Math.pow(a, regs[base + this.S(null)]); + break; + } + case 24955: + { + var dst = this.S(45); + var a = regs[base + this.S(null)]; + regs[base + dst] = a & regs[base + this.S("k")]; + break; + } + case 57871: + { + var dst = this.S(null); + var a = regs[base + this.S("q")]; + regs[base + dst] = a | regs[base + this.S([])]; + break; + } + case 5417: + { + var dst = this.S(undefined); + var a = regs[base + this.S("k")]; + regs[base + dst] = a ^ regs[base + this.S([])]; + break; + } + case 9866: + { + var dst = this.S([]); + var a = regs[base + this.S("x")]; + regs[base + dst] = a << regs[base + this.S(null)]; + break; + } + case 12140: + { + var dst = this.S(30); + var a = regs[base + this.S(null)]; + regs[base + dst] = a >> regs[base + this.S([])]; + break; + } + case 46998: + { + var dst = this.S(73); + var a = regs[base + this.S(31)]; + regs[base + dst] = a >>> regs[base + this.S(undefined)]; + break; + } + + // Comparison (dst, src1, src2) + case 18923: + { + var dst = this.S("q"); + var a = regs[base + this.S("z")]; + regs[base + dst] = a < regs[base + this.S(undefined)]; + break; + } + case 23357: + { + var dst = this.S([]); + var a = regs[base + this.S(undefined)]; + regs[base + dst] = a > regs[base + this.S(null)]; + break; + } + case 64658: + { + var dst = this.S(null); + var a = regs[base + this.S(27)]; + regs[base + dst] = a <= regs[base + this.S(undefined)]; + break; + } + case 29044: + { + var dst = this.S(48); + var a = regs[base + this.S("q")]; + regs[base + dst] = a >= regs[base + this.S(86)]; + break; + } + case 25819: + { + var dst = this.S({}); + var a = regs[base + this.S({})]; + regs[base + dst] = a === regs[base + this.S(null)]; + break; + } + case 39164: + { + var dst = this.S(undefined); + var a = regs[base + this.S({})]; + regs[base + dst] = a !== regs[base + this.S("z")]; + break; + } + case 8215: + { + var dst = this.S([]); + var a = regs[base + this.S(undefined)]; + regs[base + dst] = a == regs[base + this.S([])]; + break; + } + case 31978: + { + var dst = this.S({}); + var a = regs[base + this.S(null)]; + regs[base + dst] = a != regs[base + this.S([])]; + break; + } + case 21914: + { + var dst = this.S(undefined); + var a = regs[base + this.S(null)]; + regs[base + dst] = a in regs[base + this.S([])]; + break; + } + case 6605: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.S(94); + var obj = regs[base + this.S(undefined)]; + regs[base + dst] = obj instanceof regs[base + this.S([])]; + break; + } + + // Unary (dst, src) + case 57697: + { + var dst = this.S(undefined); + regs[base + dst] = -regs[base + this.S([])]; + break; + } + case 23644: + { + var dst = this.S(undefined); + regs[base + dst] = +regs[base + this.S(undefined)]; + break; + } + case 43518: + { + var dst = this.S(null); + regs[base + dst] = !regs[base + this.S([])]; + break; + } + case 17920: + { + var dst = this.S([]); + regs[base + dst] = ~regs[base + this.S({})]; + break; + } + case 39066: + { + var dst = this.S(null); + regs[base + dst] = typeof regs[base + this.S(null)]; + break; + } + case 2548: + { + var dst = this.S(undefined); + this.S(3); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 36661: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.S(undefined); + var name = this.E(); + var val = Object.prototype.hasOwnProperty.call(this.Y, name) ? this.Y[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39919: + regs[fp + 1] = this.S(undefined); + break; + case 37081: + { + var src = this.S(null); + var target = this.S({}); + if (!regs[base + src]) regs[fp + 1] = target; + break; + } + case 5524: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.S("k"); + var target = this.S(null); + if (regs[base + src]) regs[fp + 1] = target; + break; + } + + // Calls + case 4545: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.S(null); + var callee = regs[base + this.S(null)]; + var argc = this.S({}); + var args; + if (argc === 3847166315) { + args = regs[base + this.S(40)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.S([])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.ag({}, null, [], this.Y, dst << 1, args, closure); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 15913: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.S({}); + var receiver = regs[base + this.S(52)]; + var callee = regs[base + this.S(undefined)]; + var argc = this.S(17); + var args; + if (argc === 3847166315) { + args = regs[base + this.S([])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.S(59)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.ag(undefined, null, {}, receiver, dst << 1, args, closure); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 52616: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.S(undefined); + var callee = regs[base + this.S(32)]; + var argc = this.S(null); + var args; + if (argc === 3847166315) { + args = regs[base + this.S({})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.S(undefined)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.ag(undefined, "y", {}, Object.create(closure.prototype || null), dst << 1 | 1, args, closure); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 62456: + { + var retVal = regs[base + this.S("k")]; + this.b({}, 41, undefined, "k", fp); // must happen before frame is abandoned + + var caller = regs[fp + 10]; + var retDst = regs[fp + 8]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 2]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 4]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.R = fp; + this.P = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 19563: + throw regs[base + this.S(undefined)]; + + // Closures + case 18527: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.S("k"); + var startPc = this.S("k"); + var paramCount = this.S(null); + var regCount = this.S("z"); + var uvCount = this.S([]); + var hasRest = this.S({}); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.S("x"); + var uvIndex = this.S(75); + uvDescs[i] = { + V: isLocalRaw, + m: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + U: paramCount, + q: regCount, + ab: startPc, + z: hasRest + }; + var closure = new Closure("z", [], fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.V) { + closure.C.push(this.aa({}, fp, 23, [], uvd.m, [])); + } else { + closure.C.push(regs[fp + 3].C[uvd.m]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(21, self.v, self.X, [], {}, self.Y).t(c, Array.prototype.slice.call(arguments), null, this == null ? self.Y : this, "z", 93); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 28412: + { + // dst, count, [elemReg...] + var dst = this.S(null); + var count = this.S({}); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.S({})]; + regs[base + dst] = elems; + break; + } + case 53382: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.S([]); + var pairCount = this.S(35); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.S({})]; + var val = regs[base + this.S(40)]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 1716: + { + // obj, key, fn + var obj = regs[base + this.S(null)]; + var key = regs[base + this.S("x")]; + var getterFn = regs[base + this.S(null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 28189: + { + // obj, key, fn + var obj = regs[base + this.S(undefined)]; + var key = regs[base + this.S(null)]; + var setterFn = regs[base + this.S(undefined)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52804: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.S([]); + var obj = regs[base + this.S([])]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + D: keys, + c: 0 + }; + break; + } + case 8387: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.S(undefined); + var iter = regs[base + this.S([])]; + var exitTarget = this.S("y"); + if (iter.c >= iter.D.length) { + regs[fp + 1] = exitTarget; + } else { + regs[base + dst] = iter.D[iter.c++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 50691: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 11]; + if (!hs) regs[fp + 11] = hs = []; + hs.push({ + I: this.S("x"), + L: this.S("y") + }); + break; + } + case 40557: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 11].pop(); + break; + } + case 57952: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 11]; + if (!hs) regs[fp + 11] = hs = []; + hs.push({ + y: this.S(null), + ae: this.S(69), + B: this.S(undefined), + ad: this.S(undefined) + }); + break; + } + + // Self-modifying bytecode + case 32453: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.S(undefined); + var sliceStart = this.S("q"); + var sliceEnd = this.S(null); + var pk = this.S([]) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.v[destPc + (pi - sliceStart)] = (this.v[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 13119: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 1] = regs[base + this.S([])]; + break; + } + case 54257: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 1] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.P; + while (searchFrame) { + var handlers = regs[searchFrame + 11]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.b(null, [], [], undefined, searchFrame); + this.R = searchFrame; + searchFrame = regs[searchFrame + 10]; + this.P = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 11].pop(); + var hBase = regs[handledFrame + 7]; + if (h.L !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.L] = err; + regs[handledFrame + 1] = h.I; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.ae] = h.ad; + regs[hBase + h.B] = err; + regs[handledFrame + 1] = h.y; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.R = handledFrame + regs[handledFrame + 4]; + this.P = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.P) return this.f[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(82, decodeBytecode(BYTECODE), CONSTANTS, 31, [], globals); +vm.t(new Closure(undefined, 90, { + U: 0, + q: 12, + ab: 0 +}), null // no arguments object / parameter setup for the root frame +, 45, undefined, [], 33); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-182.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-182.js new file mode 100644 index 00000000..11fbc155 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-182.js @@ -0,0 +1,952 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) + +// Closure & Frame +function Closure(fn, fake_6) { + this.B = fn; + this.E = []; + this.prototype = {}; // <- default prototype object for `new` + this.O = fake_6; +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function VM(bytecode, fake_7, globals, constants) { + this.J = bytecode; + this.A = constants; + this.U = fake_7; + this.d = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.W = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.T = []; + this.u = 8; + this.o = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +function Upvalue(absSlot, fake_1, regs) { + this.e = fake_1; + this.T = regs; // shared reference to VM._regs flat array + this.f = absSlot; // absolute index; stable as long as frame is alive + this.p = false; + this.j = undefined; +} +var BYTECODE = [49294, 1, 10642, 5, 89, 0, 4, 1, 0, 1, 2, 12815, 3, 5, 49940, 5, 0, 12815, 2, 5, 39948, 5, 3, 0, 12815, 4, 5, 44053, 5, 0, 0, 63273, 6, 1, 0, 63273, 7, 2, 0, 39948, 8, 4, 1, 7, 63273, 7, 3, 0, 39948, 9, 4, 1, 7, 63273, 7, 4, 0, 39948, 10, 4, 1, 7, 63273, 7, 5, 0, 39948, 11, 4, 1, 7, 49940, 7, 5, 8, 9, 10, 11, 2, 5872, 5, 6, 7, 63273, 5, 6, 0, 13431, 5, 49294, 1, 63273, 3, 2, 0, 12815, 2, 3, 10642, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 13431, 3, 63273, 3, 6, 0, 13431, 3, 49294, 2, 40589, 326, 4, 5, 353, 19519, 264, 3, 41765, 7, 0, 63273, 8, 7, 0, 55279, 9, 7, 8, 63273, 8, 8, 0, 38978, 10, 8, 0, 27168, 8, 7, 9, 1, 10, 63273, 7, 2, 0, 64647, 8, 0, 7, 64871, 8, 185, 44053, 7, 9, 0, 63273, 8, 10, 0, 55279, 9, 7, 8, 63273, 8, 11, 0, 27168, 10, 7, 9, 1, 8, 63273, 7, 3, 0, 64647, 8, 0, 7, 64871, 8, 211, 63273, 7, 12, 0, 12815, 6, 7, 63273, 7, 13, 0, 55279, 8, 6, 7, 63273, 7, 4, 0, 64647, 8, 0, 7, 64871, 8, 237, 44053, 7, 14, 0, 63273, 8, 15, 0, 58650, 9, 7, 1, 8, 56258, 9, 41765, 7, 1, 63273, 8, 3, 0, 38978, 9, 7, 8, 55066, 1, 9, 13991, 12815, 5, 9, 2068, 4, 355, 13991, 7297, 326, 13991, 7297, 320, 41765, 7, 0, 63273, 8, 7, 0, 55279, 9, 7, 8, 63273, 8, 16, 0, 63273, 10, 17, 0, 55279, 11, 3, 10, 38978, 10, 8, 11, 27168, 8, 7, 9, 1, 10, 41765, 7, 1, 63273, 8, 3, 0, 38978, 9, 7, 8, 55066, 1, 9, 12815, 5, 9, 2068, 4, 357, 13991, 7297, 326, 13991, 2068, 4, 359, 7297, 326, 41765, 7, 0, 63273, 8, 7, 0, 55279, 9, 7, 8, 63273, 8, 18, 0, 38978, 10, 8, 0, 27168, 8, 7, 9, 1, 10, 24210, 4, 56258, 5, 13431, 5, 13431, 5, 63273, 7, 6, 0, 13431, 7]; +Upvalue.prototype.i = function (fake_2) { + this.Y = fake_2; + return this.p ? this.j : this.T[this.f]; +}; +Upvalue.prototype.s = function (fake_4, fake_5) { + this.j = this.T[this.f]; + this.r = fake_5; + this.p = true; + this.a = fake_4; +}; +VM.prototype.H = function (fp, fake_17) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.W; + this.N = fake_17; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this.T; + var hi = fp + regs[fp + 4]; + for (var i = regs[fp + 0]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.s(65, undefined); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.y = function (fake_3, v) { + if (this.p) this.j = v;else this.T[this.f] = v; + this.k = fake_3; +}; +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.c = function (fake_16, keyIn, idxIn) { + var idx = idxIn ?? this.L(27, null, null); + var key = keyIn ?? this.L("y", {}, undefined); + this.g = fake_16; + var v = this.A[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.V = function (fake_15, fake_13, fp, fake_14, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.T[fp + 0] + slot; + var uvs = this.W || (this.W = []); + this.w = fake_13; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, undefined, this.T)); + this.t = [fake_15, fake_14]; +}; +VM.prototype.L = function (fake_10, fake_9, fake_8) { + this.n = fake_8; + this.m = fake_9 && fake_10; + return this.J[this.T[this.o + 7]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.I = function (retDst, args, fake_11, fake_12, closure, thisVal) { + var fn = closure.B; + var regs = this.T; + var fp = this.u; + var size = 10 + fn.C; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 10; + regs[fp + 7] = fn.F; + this.G = [fake_11, fake_12]; + regs[fp + 2] = this.o; + regs[fp + 1] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 6] = closure; + regs[fp + 4] = size; + regs[fp + 0] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof 8 === "number") regs[fp + 8] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.X; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.F; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.u = end; + if (args) { + if (fn.z) { + var restSlot = fn.X - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.C; a++) regs[base + a] = args[a]; + } + if (fn.X < fn.C) regs[base + fn.X] = args; + } + this.o = fp; +}; +VM.prototype.q = function (args, fake_18, fake_19, closure, thisVal, fake_20) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.h = [fake_19, fake_20]; + this.I(0, args, 42, {}, closure, thisVal); + while (true) { + var fp = this.o; + var regs = this.T; + var bc = this.J; + var pc = regs[fp + 7]; + if (pc >= bc.length) break; + regs[fp + 7] = pc + 1; + var op = this.J[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.J.length; i++) this.J[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.u; i2++) this.T[i2] = undefined; + op = 7297; + regs[fp + 7] = this.J.length; // jump past end to halt + } + } + try { + var base = regs[fp + 0]; + + /* @SWITCH */ + switch (op) { + case 63273: + { + var dst = this.L("x", "k", {}); + regs[base + dst] = this.c(); + break; + } + case 2068: + { + var dst = this.L({}, undefined, 52); + regs[base + dst] = this.L(null, undefined, "k"); + break; + } + case 44053: + { + var dst = this.L({}, undefined, undefined); + var globalName = this.c(); + if (!(globalName in this.d)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.d[globalName]; + break; + } + case 41765: + { + var dst = this.L(null, null, undefined); + regs[base + dst] = regs[fp + 6].E[this.L(81, null, "x")].i([]); + break; + } + case 49294: + { + var dst = this.L({}, "z", null); + regs[base + dst] = regs[fp + 3]; + break; + } + case 12815: + { + var dst = this.L(null, "k", {}); + regs[base + dst] = regs[base + this.L(null, "k", "z")]; + break; + } + case 28556: + { + // globals[globalName] = regs[src] + this.d[this.c()] = regs[base + this.L({}, {}, null)]; + break; + } + case 55066: + { + var uvIdx = this.L(77, null, null); + regs[fp + 6].E[uvIdx].y(44, regs[base + this.L(undefined, 31, 81)]); + break; + } + case 55279: + { + // dst = regs[obj][regs[key]] + var dst = this.L("q", [], 51); + var obj = regs[base + this.L(undefined, {}, "y")]; + var key = regs[base + this.L(10, 37, [])]; + regs[base + dst] = obj[key]; + break; + } + case 5872: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.L({}, undefined, null)]; + var key = regs[base + this.L(undefined, "z", [])]; + var val = regs[base + this.L(75, 6, {})]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 63910: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.L(null, null, "q"); + var obj = regs[base + this.L(null, null, null)]; + var key = regs[base + this.L(null, undefined, {})]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 38978: + { + var dst = this.L(85, "k", undefined); + var a = regs[base + this.L(null, 85, null)]; + regs[base + dst] = a + regs[base + this.L([], {}, null)]; + break; + } + case 1702: + { + var dst = this.L("k", undefined, []); + var a = regs[base + this.L(null, {}, {})]; + regs[base + dst] = a - regs[base + this.L("y", undefined, 28)]; + break; + } + case 59734: + { + var dst = this.L("z", "y", undefined); + var a = regs[base + this.L(27, undefined, undefined)]; + regs[base + dst] = a * regs[base + this.L(null, [], null)]; + break; + } + case 65274: + { + var dst = this.L("k", 61, null); + var a = regs[base + this.L(84, null, {})]; + regs[base + dst] = a / regs[base + this.L(undefined, [], {})]; + break; + } + case 11271: + { + var dst = this.L(undefined, 27, {}); + var a = regs[base + this.L([], "q", null)]; + regs[base + dst] = a % regs[base + this.L("x", undefined, undefined)]; + break; + } + case 18230: + { + // Math.pow instead of `**` + var dst = this.L(80, 33, 73); + var a = regs[base + this.L(45, [], 41)]; + regs[base + dst] = Math.pow(a, regs[base + this.L([], undefined, [])]); + break; + } + case 21828: + { + var dst = this.L(null, 49, "z"); + var a = regs[base + this.L(undefined, [], {})]; + regs[base + dst] = a & regs[base + this.L("x", 35, "x")]; + break; + } + case 32609: + { + var dst = this.L(null, 78, null); + var a = regs[base + this.L(null, 35, undefined)]; + regs[base + dst] = a | regs[base + this.L("q", {}, {})]; + break; + } + case 15560: + { + var dst = this.L(null, {}, []); + var a = regs[base + this.L({}, {}, null)]; + regs[base + dst] = a ^ regs[base + this.L(undefined, undefined, "z")]; + break; + } + case 51471: + { + var dst = this.L(undefined, {}, undefined); + var a = regs[base + this.L(undefined, "q", {})]; + regs[base + dst] = a << regs[base + this.L({}, undefined, "z")]; + break; + } + case 50846: + { + var dst = this.L({}, undefined, {}); + var a = regs[base + this.L({}, undefined, [])]; + regs[base + dst] = a >> regs[base + this.L("k", "q", 47)]; + break; + } + case 44691: + { + var dst = this.L(9, {}, null); + var a = regs[base + this.L(null, undefined, {})]; + regs[base + dst] = a >>> regs[base + this.L(null, undefined, null)]; + break; + } + + // Comparison (dst, src1, src2) + case 11714: + { + var dst = this.L(62, undefined, 0); + var a = regs[base + this.L(9, 38, undefined)]; + regs[base + dst] = a < regs[base + this.L({}, {}, null)]; + break; + } + case 28433: + { + var dst = this.L(91, null, "k"); + var a = regs[base + this.L(61, [], "k")]; + regs[base + dst] = a > regs[base + this.L({}, [], undefined)]; + break; + } + case 4517: + { + var dst = this.L(null, undefined, undefined); + var a = regs[base + this.L([], undefined, undefined)]; + regs[base + dst] = a <= regs[base + this.L(undefined, [], 65)]; + break; + } + case 59488: + { + var dst = this.L("x", "q", null); + var a = regs[base + this.L(null, undefined, undefined)]; + regs[base + dst] = a >= regs[base + this.L({}, null, 72)]; + break; + } + case 64647: + { + var dst = this.L(undefined, 4, 52); + var a = regs[base + this.L({}, null, undefined)]; + regs[base + dst] = a === regs[base + this.L({}, undefined, {})]; + break; + } + case 13217: + { + var dst = this.L("q", "z", "q"); + var a = regs[base + this.L("k", null, "x")]; + regs[base + dst] = a !== regs[base + this.L({}, undefined, 12)]; + break; + } + case 45154: + { + var dst = this.L({}, [], {}); + var a = regs[base + this.L({}, 69, "k")]; + regs[base + dst] = a == regs[base + this.L({}, "q", "q")]; + break; + } + case 44273: + { + var dst = this.L(52, 27, {}); + var a = regs[base + this.L(null, undefined, null)]; + regs[base + dst] = a != regs[base + this.L([], null, null)]; + break; + } + case 23801: + { + var dst = this.L({}, [], 25); + var a = regs[base + this.L([], [], 96)]; + regs[base + dst] = a in regs[base + this.L("x", undefined, [])]; + break; + } + case 1968: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.L(null, "q", 63); + var obj = regs[base + this.L({}, undefined, "k")]; + regs[base + dst] = obj instanceof regs[base + this.L([], 72, null)]; + break; + } + + // Unary (dst, src) + case 23038: + { + var dst = this.L([], undefined, {}); + regs[base + dst] = -regs[base + this.L(undefined, [], "x")]; + break; + } + case 2315: + { + var dst = this.L(undefined, null, "x"); + regs[base + dst] = +regs[base + this.L({}, undefined, null)]; + break; + } + case 54482: + { + var dst = this.L(undefined, 22, undefined); + regs[base + dst] = !regs[base + this.L("x", {}, null)]; + break; + } + case 19322: + { + var dst = this.L(12, undefined, null); + regs[base + dst] = ~regs[base + this.L(53, {}, undefined)]; + break; + } + case 20202: + { + var dst = this.L(85, 49, null); + regs[base + dst] = typeof regs[base + this.L({}, "k", "k")]; + break; + } + case 49339: + { + var dst = this.L([], {}, undefined); + this.L("q", {}, {}); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 11916: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.L(37, 30, "z"); + var name = this.c(); + var val = Object.prototype.hasOwnProperty.call(this.d, name) ? this.d[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 7297: + regs[fp + 7] = this.L({}, "x", "q"); + break; + case 64871: + { + var src = this.L(undefined, [], "x"); + var target = this.L(undefined, undefined, []); + if (!regs[base + src]) regs[fp + 7] = target; + break; + } + case 17601: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.L(undefined, {}, "x"); + var target = this.L("k", undefined, 0); + if (regs[base + src]) regs[fp + 7] = target; + break; + } + + // Calls + case 39948: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.L("q", null, {}); + var callee = regs[base + this.L(undefined, "k", {})]; + var argc = this.L([], [], null); + var args; + if (argc === 1404279311) { + args = regs[base + this.L({}, undefined, "y")]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.L([], undefined, 3)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.I(dst << 1, args, {}, null, closure, this.d); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 27168: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.L([], "q", "z"); + var receiver = regs[base + this.L(undefined, 30, [])]; + var callee = regs[base + this.L(33, null, {})]; + var argc = this.L({}, [], {}); + var args; + if (argc === 1404279311) { + args = regs[base + this.L("q", {}, undefined)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.L(null, 57, "x")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.I(dst << 1, args, null, [], closure, receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 58650: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.L([], undefined, null); + var callee = regs[base + this.L("z", null, {})]; + var argc = this.L({}, null, []); + var args; + if (argc === 1404279311) { + args = regs[base + this.L("k", undefined, {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.L(25, "x", "y")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.I(dst << 1 | 1, args, [], {}, closure, Object.create(closure.prototype || null)); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 13431: + { + var retVal = regs[base + this.L(undefined, {}, [])]; + this.H(fp, 53); // must happen before frame is abandoned + + var caller = regs[fp + 2]; + var retDst = regs[fp + 1]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 4]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.u = fp; + this.o = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 0] + (retDst >> 1) : 0] = retVal; + break; + } + case 56258: + throw regs[base + this.L(undefined, null, [])]; + + // Closures + case 10642: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.L(undefined, "y", "k"); + var startPc = this.L(11, null, undefined); + var paramCount = this.L("x", undefined, "x"); + var regCount = this.L({}, undefined, []); + var uvCount = this.L("q", null, {}); + var hasRest = this.L([], [], {}); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.L({}, "q", {}); + var uvIndex = this.L([], 91, undefined); + uvDescs[i] = { + l: isLocalRaw, + D: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + X: paramCount, + C: regCount, + F: startPc, + z: hasRest + }; + var closure = new Closure(fn, []); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.l) { + closure.E.push(this.V([], undefined, fp, [], uvd.D)); + } else { + closure.E.push(regs[fp + 6].E[uvd.D]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.J, [], self.d, self.A).q(Array.prototype.slice.call(arguments), [], "k", c, this == null ? self.d : this, 82); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 49940: + { + // dst, count, [elemReg...] + var dst = this.L(24, undefined, {}); + var count = this.L({}, null, "q"); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.L("k", "q", null)]; + regs[base + dst] = elems; + break; + } + case 64668: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.L(null, null, 25); + var pairCount = this.L({}, {}, 2); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.L([], undefined, [])]; + var val = regs[base + this.L([], null, [])]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 24330: + { + // obj, key, fn + var obj = regs[base + this.L({}, [], null)]; + var key = regs[base + this.L([], null, undefined)]; + var getterFn = regs[base + this.L("y", "x", undefined)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 64651: + { + // obj, key, fn + var obj = regs[base + this.L(14, {}, [])]; + var key = regs[base + this.L(undefined, 75, [])]; + var setterFn = regs[base + this.L(46, 47, null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 65017: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.L(null, 71, []); + var obj = regs[base + this.L(null, {}, [])]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + P: keys, + S: 0 + }; + break; + } + case 10020: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.L({}, "q", {}); + var iter = regs[base + this.L("z", 63, 5)]; + var exitTarget = this.L({}, {}, "k"); + if (iter.S >= iter.P.length) { + regs[fp + 7] = exitTarget; + } else { + regs[base + dst] = iter.P[iter.S++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 19519: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 9]; + if (!hs) regs[fp + 9] = hs = []; + hs.push({ + R: this.L(undefined, {}, undefined), + b: this.L([], 72, 56) + }); + break; + } + case 13991: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 9].pop(); + break; + } + case 40589: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 9]; + if (!hs) regs[fp + 9] = hs = []; + hs.push({ + x: this.L(undefined, 66, 29), + M: this.L({}, [], 64), + K: this.L("q", 39, {}), + v: this.L(null, [], null) + }); + break; + } + + // Self-modifying bytecode + case 1001: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.L([], [], 2); + var sliceStart = this.L("z", {}, {}); + var sliceEnd = this.L(59, [], {}); + var pk = this.L([], null, undefined) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.J[destPc + (pi - sliceStart)] = (this.J[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 24210: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 7] = regs[base + this.L([], 90, "x")]; + break; + } + case 27223: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 7] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.o; + while (searchFrame) { + var handlers = regs[searchFrame + 9]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.H(searchFrame, 59); + this.u = searchFrame; + searchFrame = regs[searchFrame + 2]; + this.o = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 9].pop(); + var hBase = regs[handledFrame + 0]; + if (h.b !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.b] = err; + regs[handledFrame + 7] = h.R; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.M] = h.v; + regs[hBase + h.K] = err; + regs[handledFrame + 7] = h.x; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.u = handledFrame + regs[handledFrame + 4]; + this.o = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.o) return this.T[0]; + } + this.Q = fake_18; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), null, globals, CONSTANTS); +vm.q(null // no arguments object / parameter setup for the root frame +, {}, {}, new Closure({ + X: 0, + C: 12, + F: 0 +}, undefined), undefined, null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-183.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-183.js new file mode 100644 index 00000000..d779124f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-183.js @@ -0,0 +1,958 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(absSlot, fake_4, fake_2, fake_3, fake_1, regs) { + this.e = fake_1; + this.p = regs; // shared reference to VM._regs flat array + this.n = fake_4; + this.r = absSlot; // absolute index; stable as long as frame is alive + this.g = false; + this.ab = fake_3; + this.y = fake_2; + this.ag = undefined; +} +// Closure & Frame +function Closure(fn, fake_11) { + this.z = fn; + this.ad = []; + this.prototype = {}; // <- default prototype object for `new` + this.h = fake_11; +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, fake_12, globals) { + this.b = bytecode; + this.j = constants; + this.f = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.k = fake_12; + this.U = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.p = []; + this.S = 7; + this.m = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +var BYTECODE = [63827, 1, 63522, 5, 89, 0, 4, 1, 0, 1, 2, 25408, 3, 5, 57913, 5, 0, 25408, 2, 5, 45847, 5, 3, 0, 25408, 4, 5, 38527, 5, 0, 0, 1783, 6, 1, 0, 1783, 7, 2, 0, 45847, 8, 4, 1, 7, 1783, 7, 3, 0, 45847, 9, 4, 1, 7, 1783, 7, 4, 0, 45847, 10, 4, 1, 7, 1783, 7, 5, 0, 45847, 11, 4, 1, 7, 57913, 7, 5, 8, 9, 10, 11, 2, 42057, 5, 6, 7, 1783, 5, 6, 0, 4387, 5, 63827, 1, 1783, 3, 2, 0, 25408, 2, 3, 63522, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 4387, 3, 1783, 3, 6, 0, 4387, 3, 63827, 2, 34663, 326, 4, 5, 353, 60503, 264, 3, 41838, 7, 0, 1783, 8, 7, 0, 1227, 9, 7, 8, 1783, 8, 8, 0, 60230, 10, 8, 0, 6030, 8, 7, 9, 1, 10, 1783, 7, 2, 0, 11047, 8, 0, 7, 55595, 8, 185, 38527, 7, 9, 0, 1783, 8, 10, 0, 1227, 9, 7, 8, 1783, 8, 11, 0, 6030, 10, 7, 9, 1, 8, 1783, 7, 3, 0, 11047, 8, 0, 7, 55595, 8, 211, 1783, 7, 12, 0, 25408, 6, 7, 1783, 7, 13, 0, 1227, 8, 6, 7, 1783, 7, 4, 0, 11047, 8, 0, 7, 55595, 8, 237, 38527, 7, 14, 0, 1783, 8, 15, 0, 51590, 9, 7, 1, 8, 6897, 9, 41838, 7, 1, 1783, 8, 3, 0, 60230, 9, 7, 8, 53456, 1, 9, 10682, 25408, 5, 9, 36363, 4, 355, 10682, 19772, 326, 10682, 19772, 320, 41838, 7, 0, 1783, 8, 7, 0, 1227, 9, 7, 8, 1783, 8, 16, 0, 1783, 10, 17, 0, 1227, 11, 3, 10, 60230, 10, 8, 11, 6030, 8, 7, 9, 1, 10, 41838, 7, 1, 1783, 8, 3, 0, 60230, 9, 7, 8, 53456, 1, 9, 25408, 5, 9, 36363, 4, 357, 10682, 19772, 326, 10682, 36363, 4, 359, 19772, 326, 41838, 7, 0, 1783, 8, 7, 0, 1227, 9, 7, 8, 1783, 8, 18, 0, 60230, 10, 8, 0, 6030, 8, 7, 9, 1, 10, 41819, 4, 6897, 5, 4387, 5, 4387, 5, 1783, 7, 6, 0, 4387, 7]; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CLOSURE_MAP = new WeakMap(); +VM.prototype.a = function (fake_16, fake_13, fake_14, fake_15) { + return this.b[this.p[this.m + 4]++]; + this.c = [fake_13, fake_16]; + this.J = [fake_15, fake_14]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.Q = function (retDst, fake_19, thisVal, fake_17, args, closure, fake_18) { + var fn = closure.z; + var regs = this.p; + var fp = this.S; + var size = 16 + fn.l; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 16; + regs[fp + 4] = fn.H; + regs[fp + 13] = this.m; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + this.af = fake_17 || fake_18; + regs[fp + 8] = closure; + regs[fp + 15] = size; + regs[fp + 10] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + this.L = fake_19; + if (typeof 12 === "number") regs[fp + 12] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.V; + if (typeof 7 === "number") regs[fp + 7] = args; + if (typeof 1 === "number") regs[fp + 1] = fn.H; + if (typeof 9 === "number") regs[fp + 9] = 0; + this.S = end; + if (args) { + if (fn.ae) { + var restSlot = fn.V - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.l; a++) regs[base + a] = args[a]; + } + if (fn.V < fn.l) regs[base + fn.V] = args; + } + this.m = fp; +}; +Upvalue.prototype.t = function (fake_7, fake_9, fake_10, fake_8) { + this.F = fake_10; + this.ag = this.p[this.r]; + this.g = true; + this.R = fake_7; + this.ac = fake_8; + this.D = fake_9; +}; +VM.prototype.P = function (fake_25, fake_26, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.U; + if (!uvs) return; // nothing in this program ever captured a local + this.T = fake_25 && fake_26; + var regs = this.p; + var hi = fp + regs[fp + 15]; + for (var i = regs[fp + 10]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.t("x", undefined, undefined, 53); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.G = function (fake_23, keyIn, fake_24, idxIn) { + var idx = idxIn ?? this.a(62, 27, null, "k"); + var key = keyIn ?? this.a({}, {}, "y", undefined); + var v = this.j[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + this.M = fake_24; + var out = ""; + this.E = fake_23; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +Upvalue.prototype.i = function (fake_5) { + return this.g ? this.ag : this.p[this.r]; + this.v = fake_5; +}; +VM.prototype.W = function (slot, fake_21, fake_20, fake_22, fp) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.p[fp + 10] + slot; + this.u = fake_21; + var uvs = this.U || (this.U = []); + this.Z = fake_22 && fake_20; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, 24, undefined, null, [], this.p)); +}; +Upvalue.prototype.B = function (v, fake_6) { + if (this.g) this.ag = v;else this.p[this.r] = v; + this.d = fake_6; +}; +VM.prototype.I = function (closure, fake_28, thisVal, fake_27, fake_29, fake_30, args) { + var now = () => { + return performance.now(); + }; + this.X = [fake_27, fake_29]; + this.K = fake_28; + var lastTime = now(); + this.Q(0, undefined, thisVal, [], args, closure, {}); + while (true) { + var fp = this.m; + var regs = this.p; + var bc = this.b; + var pc = regs[fp + 4]; + if (pc >= bc.length) break; + regs[fp + 4] = pc + 1; + var op = this.b[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof 1 === "number") regs[fp + 1] = (regs[fp + 1] + 1) % bc.length; + if (typeof 9 === "number") regs[fp + 9]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.b.length; i++) this.b[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.S; i2++) this.p[i2] = undefined; + op = 19772; + regs[fp + 4] = this.b.length; // jump past end to halt + } + } + try { + var base = regs[fp + 10]; + + /* @SWITCH */ + switch (op) { + case 1783: + { + var dst = this.a([], [], [], null); + regs[base + dst] = this.G(); + break; + } + case 36363: + { + var dst = this.a(undefined, "k", [], null); + regs[base + dst] = this.a(undefined, {}, null, {}); + break; + } + case 38527: + { + var dst = this.a([], "k", "z", {}); + var globalName = this.G(); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; + break; + } + case 41838: + { + var dst = this.a(undefined, [], null, null); + regs[base + dst] = regs[fp + 8].ad[this.a("y", "k", null, 93)].i(42); + break; + } + case 63827: + { + var dst = this.a(34, [], {}, []); + regs[base + dst] = regs[fp + 3]; + break; + } + case 25408: + { + var dst = this.a(undefined, {}, null, 4); + regs[base + dst] = regs[base + this.a([], null, {}, {})]; + break; + } + case 51321: + { + // globals[globalName] = regs[src] + this.f[this.G()] = regs[base + this.a({}, "k", {}, "y")]; + break; + } + case 53456: + { + var uvIdx = this.a("y", undefined, undefined, null); + regs[fp + 8].ad[uvIdx].B(regs[base + this.a({}, "q", null, [])], null); + break; + } + case 1227: + { + // dst = regs[obj][regs[key]] + var dst = this.a(null, [], undefined, null); + var obj = regs[base + this.a({}, "y", {}, "k")]; + var key = regs[base + this.a({}, [], "z", null)]; + regs[base + dst] = obj[key]; + break; + } + case 42057: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.a(83, {}, {}, null)]; + var key = regs[base + this.a([], [], [], 70)]; + var val = regs[base + this.a(undefined, undefined, undefined, null)]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 65260: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.a(undefined, "q", null, undefined); + var obj = regs[base + this.a(76, undefined, 41, 95)]; + var key = regs[base + this.a([], undefined, null, [])]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 60230: + { + var dst = this.a(undefined, {}, {}, []); + var a = regs[base + this.a({}, 19, undefined, "x")]; + regs[base + dst] = a + regs[base + this.a([], "k", undefined, {})]; + break; + } + case 3760: + { + var dst = this.a({}, 28, [], null); + var a = regs[base + this.a({}, {}, undefined, {})]; + regs[base + dst] = a - regs[base + this.a(undefined, {}, [], [])]; + break; + } + case 56749: + { + var dst = this.a({}, 37, 45, []); + var a = regs[base + this.a(null, undefined, "z", 43)]; + regs[base + dst] = a * regs[base + this.a(undefined, undefined, 5, [])]; + break; + } + case 35397: + { + var dst = this.a([], "y", 34, "z"); + var a = regs[base + this.a("k", 54, "z", undefined)]; + regs[base + dst] = a / regs[base + this.a(65, 43, undefined, "x")]; + break; + } + case 38326: + { + var dst = this.a(null, {}, {}, 56); + var a = regs[base + this.a(undefined, [], 89, undefined)]; + regs[base + dst] = a % regs[base + this.a(null, null, 91, {})]; + break; + } + case 52601: + { + // Math.pow instead of `**` + var dst = this.a("q", null, 3, 16); + var a = regs[base + this.a("q", "q", [], {})]; + regs[base + dst] = Math.pow(a, regs[base + this.a(43, {}, undefined, 79)]); + break; + } + case 7156: + { + var dst = this.a(null, "x", 43, {}); + var a = regs[base + this.a(null, 42, {}, null)]; + regs[base + dst] = a & regs[base + this.a("q", 14, "y", undefined)]; + break; + } + case 33721: + { + var dst = this.a(null, "k", null, []); + var a = regs[base + this.a(20, null, undefined, 75)]; + regs[base + dst] = a | regs[base + this.a(undefined, null, null, "y")]; + break; + } + case 40723: + { + var dst = this.a(undefined, null, null, []); + var a = regs[base + this.a("z", 84, null, undefined)]; + regs[base + dst] = a ^ regs[base + this.a("z", [], "y", null)]; + break; + } + case 46661: + { + var dst = this.a(null, "x", "y", undefined); + var a = regs[base + this.a({}, 39, {}, undefined)]; + regs[base + dst] = a << regs[base + this.a(undefined, undefined, {}, [])]; + break; + } + case 9322: + { + var dst = this.a(null, {}, undefined, []); + var a = regs[base + this.a({}, [], 20, {})]; + regs[base + dst] = a >> regs[base + this.a([], [], 2, null)]; + break; + } + case 64414: + { + var dst = this.a(62, "q", undefined, []); + var a = regs[base + this.a(null, 78, 36, {})]; + regs[base + dst] = a >>> regs[base + this.a(undefined, null, null, [])]; + break; + } + + // Comparison (dst, src1, src2) + case 41605: + { + var dst = this.a("x", null, 72, "q"); + var a = regs[base + this.a(undefined, undefined, null, "q")]; + regs[base + dst] = a < regs[base + this.a(undefined, undefined, "x", 64)]; + break; + } + case 5756: + { + var dst = this.a(null, null, {}, null); + var a = regs[base + this.a(undefined, "z", null, {})]; + regs[base + dst] = a > regs[base + this.a(undefined, [], undefined, {})]; + break; + } + case 40098: + { + var dst = this.a(null, [], [], 97); + var a = regs[base + this.a(undefined, undefined, "x", 56)]; + regs[base + dst] = a <= regs[base + this.a(71, undefined, "z", "y")]; + break; + } + case 171: + { + var dst = this.a(null, [], [], []); + var a = regs[base + this.a("z", undefined, "x", undefined)]; + regs[base + dst] = a >= regs[base + this.a("x", {}, {}, null)]; + break; + } + case 11047: + { + var dst = this.a(null, 58, "k", null); + var a = regs[base + this.a({}, undefined, null, {})]; + regs[base + dst] = a === regs[base + this.a([], {}, [], null)]; + break; + } + case 38434: + { + var dst = this.a(undefined, {}, 58, null); + var a = regs[base + this.a(null, undefined, {}, undefined)]; + regs[base + dst] = a !== regs[base + this.a(null, {}, "z", null)]; + break; + } + case 23794: + { + var dst = this.a(undefined, undefined, {}, null); + var a = regs[base + this.a(4, 80, "y", "k")]; + regs[base + dst] = a == regs[base + this.a("y", [], {}, [])]; + break; + } + case 52535: + { + var dst = this.a(null, [], null, null); + var a = regs[base + this.a(78, {}, [], undefined)]; + regs[base + dst] = a != regs[base + this.a(76, "y", null, "x")]; + break; + } + case 12233: + { + var dst = this.a(null, undefined, [], []); + var a = regs[base + this.a({}, "k", 47, undefined)]; + regs[base + dst] = a in regs[base + this.a(undefined, "q", "k", null)]; + break; + } + case 5881: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.a({}, {}, [], 56); + var obj = regs[base + this.a(55, {}, null, 1)]; + regs[base + dst] = obj instanceof regs[base + this.a(undefined, {}, {}, 98)]; + break; + } + + // Unary (dst, src) + case 23545: + { + var dst = this.a({}, {}, 88, {}); + regs[base + dst] = -regs[base + this.a({}, 78, undefined, null)]; + break; + } + case 42420: + { + var dst = this.a([], "z", undefined, 46); + regs[base + dst] = +regs[base + this.a(85, null, undefined, undefined)]; + break; + } + case 25347: + { + var dst = this.a({}, 70, null, "x"); + regs[base + dst] = !regs[base + this.a({}, {}, {}, null)]; + break; + } + case 7367: + { + var dst = this.a({}, "k", 74, []); + regs[base + dst] = ~regs[base + this.a([], undefined, [], {})]; + break; + } + case 42831: + { + var dst = this.a(8, {}, undefined, []); + regs[base + dst] = typeof regs[base + this.a(undefined, 51, [], [])]; + break; + } + case 41502: + { + var dst = this.a([], null, [], null); + this.a(80, "q", [], undefined); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 62003: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.a([], undefined, [], "y"); + var name = this.G(); + var val = Object.prototype.hasOwnProperty.call(this.f, name) ? this.f[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 19772: + regs[fp + 4] = this.a(undefined, null, 53, undefined); + break; + case 55595: + { + var src = this.a(null, undefined, "q", undefined); + var target = this.a("q", [], {}, {}); + if (!regs[base + src]) regs[fp + 4] = target; + break; + } + case 25181: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.a([], 30, "k", 23); + var target = this.a({}, [], {}, undefined); + if (regs[base + src]) regs[fp + 4] = target; + break; + } + + // Calls + case 45847: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.a("k", null, [], null); + var callee = regs[base + this.a(null, [], "k", {})]; + var argc = this.a([], [], [], null); + var args; + if (argc === 2617719534) { + args = regs[base + this.a(null, 94, "k", {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.a([], 39, null, {})]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.Q(dst << 1, [], this.f, 91, args, closure, "y"); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 6030: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.a({}, [], "k", 15); + var receiver = regs[base + this.a(null, "k", [], 45)]; + var callee = regs[base + this.a(null, null, "z", null)]; + var argc = this.a(80, [], null, undefined); + var args; + if (argc === 2617719534) { + args = regs[base + this.a(undefined, "z", "x", [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.a(null, undefined, 58, undefined)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.Q(dst << 1, 40, receiver, {}, args, closure, {}); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 51590: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.a({}, "k", {}, null); + var callee = regs[base + this.a(null, {}, [], 98)]; + var argc = this.a("z", "z", {}, {}); + var args; + if (argc === 2617719534) { + args = regs[base + this.a(58, "z", undefined, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.a(undefined, {}, "q", "x")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.Q(dst << 1 | 1, 70, Object.create(closure.prototype || null), null, args, closure, null); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 4387: + { + var retVal = regs[base + this.a(undefined, undefined, null, {})]; + this.P(96, "y", fp); // must happen before frame is abandoned + + var caller = regs[fp + 13]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 15]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.S = fp; + this.m = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 10] + (retDst >> 1) : 0] = retVal; + break; + } + case 6897: + throw regs[base + this.a(undefined, null, "y", {})]; + + // Closures + case 63522: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.a({}, [], "k", 56); + var startPc = this.a(null, 73, {}, {}); + var paramCount = this.a(48, 32, null, []); + var regCount = this.a(undefined, {}, 94, null); + var uvCount = this.a({}, undefined, [], undefined); + var hasRest = this.a(null, undefined, {}, "z"); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.a(null, {}, 55, 41); + var uvIndex = this.a([], null, [], null); + uvDescs[i] = { + s: isLocalRaw, + o: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + V: paramCount, + l: regCount, + H: startPc, + ae: hasRest + }; + var closure = new Closure(fn, 68); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.s) { + closure.ad.push(this.W(uvd.o, [], null, undefined, fp)); + } else { + closure.ad.push(regs[fp + 8].ad[uvd.o]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.b, self.j, null, self.f).I(c, undefined, this == null ? self.f : this, null, [], "y", Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 57913: + { + // dst, count, [elemReg...] + var dst = this.a(undefined, {}, "x", null); + var count = this.a(undefined, [], undefined, []); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.a({}, null, null, [])]; + regs[base + dst] = elems; + break; + } + case 51230: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.a(34, "x", null, null); + var pairCount = this.a([], null, "y", []); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.a(54, [], "y", 15)]; + var val = regs[base + this.a(undefined, 8, "x", null)]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50853: + { + // obj, key, fn + var obj = regs[base + this.a(40, {}, undefined, {})]; + var key = regs[base + this.a(89, null, "q", undefined)]; + var getterFn = regs[base + this.a([], "q", {}, {})]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 9477: + { + // obj, key, fn + var obj = regs[base + this.a({}, {}, "y", 10)]; + var key = regs[base + this.a("y", {}, {}, {})]; + var setterFn = regs[base + this.a({}, [], "k", "y")]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 48242: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.a(undefined, "y", [], {}); + var obj = regs[base + this.a({}, "q", null, {})]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + x: keys, + O: 0 + }; + break; + } + case 39882: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.a(undefined, [], {}, undefined); + var iter = regs[base + this.a(44, 1, [], [])]; + var exitTarget = this.a(4, [], [], "y"); + if (iter.O >= iter.x.length) { + regs[fp + 4] = exitTarget; + } else { + regs[base + dst] = iter.x[iter.O++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 60503: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + Y: this.a({}, "y", undefined, []), + aa: this.a(97, 10, [], []) + }); + break; + } + case 10682: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 34663: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + A: this.a({}, {}, 52, null), + C: this.a(95, [], 72, "q"), + q: this.a(6, [], "x", null), + N: this.a({}, {}, {}, null) + }); + break; + } + + // Self-modifying bytecode + case 19927: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.a("k", undefined, 79, undefined); + var sliceStart = this.a(null, {}, undefined, undefined); + var sliceEnd = this.a("y", 66, {}, {}); + var pk = this.a([], undefined, null, undefined) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.b[destPc + (pi - sliceStart)] = (this.b[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 41819: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 4] = regs[base + this.a({}, undefined, [], 97)]; + break; + } + case 49100: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 4] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.m; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.P("z", undefined, searchFrame); + this.S = searchFrame; + searchFrame = regs[searchFrame + 13]; + this.m = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 10]; + if (h.aa !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.aa] = err; + regs[handledFrame + 4] = h.Y; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.C] = h.N; + regs[hBase + h.q] = err; + regs[handledFrame + 4] = h.A; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.S = handledFrame + regs[handledFrame + 15]; + this.m = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.m) return this.p[0]; + } + this.w = fake_30; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, undefined, globals); +vm.I(new Closure({ + V: 0, + l: 12, + H: 0 +}, {}), null, undefined, null, undefined, [], null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-191.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-191.js new file mode 100644 index 00000000..b0fa6237 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-191.js @@ -0,0 +1,20 @@ +function z(a,f,g){this.e=g;this.m=f;this.b=a;this.p=null;this.g=[];this.h=7;this.s=0}var A=new WeakMap;function B(a){this.F=a;this.x=[];this.prototype={}}function da(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})}function ea(a,f){this.g=a;this.j=f;this.l=!1;this.E=[];this.z=void 0} +function C(a,f,g,b,k){for(var e=b.F,l=a.g,m=a.h,c=11+e.i,d=m+c;l.length>>13)&65535);return f}function fa(a){a.r="q";a.k=void 0;return a.l?a.z:a.g[a.j]}function P(a,f){var g=a.p;if(g){var b=a.g;a=f+b[f+7];for(f=b[f+2];f=a.e.length)break;b[e+10]=l+1;var m=a.e[l];b[e+6]=b[e+6]^l|0;try{var c=b[e+2];switch(m){case 2404:var d=a.d(null);b[c+d]=I(a);break;case 47031:d=a.d([]);b[c+d]=a.d([]);break;case 37139:d=a.d(null);var p=I(a);if(!(p in a.b))throw new ReferenceError(`${p} is not defined`);b[c+d]=a.b[p];break;case 41872:d=a.d(void 0);b[c+d]=fa(b[e+1].x[a.d("k")]);break;case 36065:d=a.d(void 0);b[c+d]=b[e+8];break;case 20288:d=a.d(void 0); +b[c+d]=b[c+a.d({})];break;case 50365:a.b[I(a)]=b[c+a.d("k")];break;case 17862:var ja=a.d("k");ha(b[e+1].x[ja],b[c+a.d([])]);break;case 18366:d=a.d(void 0);var n=b[c+a.d(void 0)],q=b[c+a.d({})];b[c+d]=n[q];break;case 15022:n=b[c+a.d(84)];q=b[c+a.d(void 0)];var w=b[c+a.d({})];Reflect.set(n,q,w);break;case 32124:d=a.d(void 0);n=b[c+a.d([])];q=b[c+a.d("z")];b[c+d]=delete n[q];break;case 3623:d=a.d("z");var h=b[c+a.d(73)];b[c+d]=h+b[c+a.d([])];break;case 10183:d=a.d(81);h=b[c+a.d(void 0)];b[c+d]=h-b[c+ +a.d({})];break;case 7357:d=a.d([]);h=b[c+a.d("x")];b[c+d]=h*b[c+a.d({})];break;case 54961:d=a.d("z");h=b[c+a.d(void 0)];b[c+d]=h/b[c+a.d("x")];break;case 21305:d=a.d(62);h=b[c+a.d(49)];b[c+d]=h%b[c+a.d("z")];break;case 20159:d=a.d(void 0);h=b[c+a.d({})];b[c+d]=Math.pow(h,b[c+a.d("k")]);break;case 13292:d=a.d(null);h=b[c+a.d({})];b[c+d]=h&b[c+a.d({})];break;case 57867:d=a.d(void 0);h=b[c+a.d(24)];b[c+d]=h|b[c+a.d([])];break;case 60028:d=a.d(null);h=b[c+a.d(null)];b[c+d]=h^b[c+a.d(76)];break;case 16709:d= +a.d(10);h=b[c+a.d([])];b[c+d]=h<>b[c+a.d(void 0)];break;case 56840:d=a.d(85);h=b[c+a.d({})];b[c+d]=h>>>b[c+a.d(68)];break;case 18329:d=a.d(37);h=b[c+a.d({})];b[c+d]=hb[c+a.d(15)];break;case 43634:d=a.d([]);h=b[c+a.d(void 0)];b[c+d]=h<=b[c+a.d(54)];break;case 17626:d=a.d(void 0);h=b[c+a.d([])];b[c+d]=h>=b[c+a.d(void 0)];break;case 49949:d=a.d(void 0);h=b[c+a.d(null)]; +b[c+d]=h===b[c+a.d("k")];break;case 5541:d=a.d({});h=b[c+a.d(void 0)];b[c+d]=h!==b[c+a.d(void 0)];break;case 24760:d=a.d([]);h=b[c+a.d("q")];b[c+d]=h==b[c+a.d(void 0)];break;case 39806:d=a.d(null);h=b[c+a.d({})];b[c+d]=h!=b[c+a.d(51)];break;case 9427:d=a.d(void 0);h=b[c+a.d(void 0)];b[c+d]=h in b[c+a.d("x")];break;case 38412:d=a.d(50);n=b[c+a.d(void 0)];b[c+d]=n instanceof b[c+a.d(null)];break;case 24086:d=a.d("k");b[c+d]=-b[c+a.d(84)];break;case 14554:d=a.d(void 0);b[c+d]=+b[c+a.d(void 0)];break; +case 2552:d=a.d(null);b[c+d]=!b[c+a.d([])];break;case 23691:d=a.d(void 0);b[c+d]=~b[c+a.d("k")];break;case 48768:d=a.d("k");b[c+d]=typeof b[c+a.d({})];break;case 19131:d=a.d({});a.d([]);b[c+d]=void 0;break;case 64685:d=a.d(51);var S=I(a);w=Object.prototype.hasOwnProperty.call(a.b,S)?a.b[S]:void 0;b[c+d]=typeof w;break;case 53096:b[e+10]=a.d("y");break;case 49894:var J=a.d([]),K=a.d(0);b[c+J]||(b[e+10]=K);break;case 21205:J=a.d([]);K=a.d(26);b[c+J]&&(b[e+10]=K);break;case 55437:d=a.d(7);var r=b[c+ +a.d(void 0)],t=a.d(void 0);if(t===1265226110)g=b[c+a.d(void 0)];else for(g=Array(t),k=0;k>1):0]=D;break;case 52724:throw b[c+a.d([])];case 35774:d=a.d("y");var la=a.d("z"),ma=a.d(null),na=a.d({}),V=a.d(null),oa=a.d([]),M=Array(V);for(k=0;k=G.f.length?b[e+10]=ua:b[c+d]=G.f[G.o++];break;case 43507:var v=b[e+4];v||(b[e+4]=v=[]);v.push({v:a.d([]),t:a.d("q")});break;case 25651:b[e+4].pop();break;case 19248:(v=b[e+4])||(b[e+4]= +v=[]);v.push({B:a.d(null),n:a.d({}),Q:a.d(null),A:a.d(void 0)});break;case 46991:var ba=a.d({}),ca=a.d(null),va=a.d("k"),H=a.d([])^ba|0;for(e=ca;e>>13)>>>0;break;case 62264:b[e+10]=b[c+a.d(48)];break;case 35145:debugger;break;default:throw Error("Unknown opcode: "+m+" at pc "+(b[e+10]-1));}}catch(x){e=0;for(l=a.s;l;){if((m=b[l+4])&&m.length>0){e=l;break}P(a,l);a.h=l;l=b[l+5];a.s=l}if(!e)throw x;l=b[e+4].pop();m=b[e+2];l.t!==void 0?(b[m+l.t]=x,b[e+ +10]=l.v):(b[m+l.n]=l.A,b[m+l.Q]=x,b[e+10]=l.B);a.h=e+b[e+7];a.s=e}if(!a.s)return a.g[0]}}var R=globalThis;typeof window!=="undefined"&&(R.window=window,R.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(R.module=module,R.exports=typeof exports!=="undefined"?exports:void 0); +Q(new z(R,["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],[36065,1,35774,5,89,0,4,1,0,1,2,20288,3,5,53940,5,0,20288,2,5,55437,5,3,0,20288,4,5,37139,5,0,0,2404,6,1,0,2404,7,2,0,55437,8,4,1,7,2404,7,3,0,55437,9,4,1,7,2404,7,4,0,55437,10,4,1,7,2404,7,5,0,55437,11,4,1,7,53940,7,5,8,9,10,11,2,15022,5,6,7,2404,5,6,0,54001,5,36065,1,2404,3,2,0,20288,2,3,35774,3,117,1,12,2,0,0,0,1,2,54001,3,2404,3,6,0,54001,3,36065,2,19248, +326,4,5,353,43507,264,3,41872,7,0,2404,8,7,0,18366,9,7,8,2404,8,8,0,3623,10,8,0,16460,8,7,9,1,10,2404,7,2,0,49949,8,0,7,49894,8,185,37139,7,9,0,2404,8,10,0,18366,9,7,8,2404,8,11,0,16460,10,7,9,1,8,2404,7,3,0,49949,8,0,7,49894,8,211,2404,7,12,0,20288,6,7,2404,7,13,0,18366,8,6,7,2404,7,4,0,49949,8,0,7,49894,8,237,37139,7,14,0,2404,8,15,0,63349,9,7,1,8,52724,9,41872,7,1,2404,8,3,0,3623,9,7,8,17862,1,9,25651,20288,5,9,47031,4,355,25651,53096,326,25651,53096,320,41872,7,0,2404,8,7,0,18366,9,7,8,2404,8, +16,0,2404,10,17,0,18366,11,3,10,3623,10,8,11,16460,8,7,9,1,10,41872,7,1,2404,8,3,0,3623,9,7,8,17862,1,9,20288,5,9,47031,4,357,25651,53096,326,25651,47031,4,359,53096,326,41872,7,0,2404,8,7,0,18366,9,7,8,2404,8,18,0,3623,10,8,0,16460,8,7,9,1,10,62264,4,52724,5,54001,5,54001,5,2404,7,6,0,54001,7]),new B({c:0,i:12,u:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-192.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-192.js new file mode 100644 index 00000000..2fcec8cc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-192.js @@ -0,0 +1,21 @@ +function x(a){this.G=a;this.l=[];this.prototype={}}function B(a,f,g,b,e){this.i=e;this.D=f;this.k=g;this.u=null;this.g=[];this.a=[a,b];this.j=2;this.h=0}function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})}var C=new WeakMap;function D(a,f){this.g=f;this.o=a;this.q=!1;this.v=void 0} +function E(a,f,g,b,e,k){for(var l=e.G,m=a.g,c=a.j,d=10+l.m,u=c+d;m.length>>13)&65535);return f}function fa(a){a.c=void 0;return a.q?a.v:a.g[a.o]}B.prototype.p=function(){return this.i[this.g[this.h+8]++]};B.prototype.z=function(a,f,g,b,e,k){a=this.g[k+7]+f;f=this.u||(this.u=[]);return f[a]||(f[a]=new D(a,this.g))}; +D.prototype.s=function(a,f){this.q?this.v=f:this.g[this.o]=f;this.w=a};B.prototype.y=function(a,f,g,b){if(f=this.u){var e=this.g;this.f=b;b=g+e[g+9];this.n=a;for(a=e[g+7];a=l.length)break;b[e+8]=m+1;m=a.i[m];b[e+5]=(b[e+5]+1)%l.length;try{var c=b[e+7];switch(m){case 5311:var d=a.p({},null);b[c+d]=Q(a);break;case 29785:d=a.p(void 0,{});b[c+d]=a.p([],14);break;case 41170:d=a.p(89,"x");var u=Q(a);if(!(u in a.k))throw new ReferenceError(`${u} is not defined`);b[c+d]=a.k[u];break;case 56573:d=a.p("q",null);b[c+d]=fa(b[e+4].l[a.p("y",null)]);break;case 56670:d=a.p([],void 0);b[c+ +d]=b[e+2];break;case 44637:d=a.p(42,"y");b[c+d]=b[c+a.p({},void 0)];break;case 20843:a.k[Q(a)]=b[c+a.p({},[])];break;case 2347:var q=a.p("y",{});b[e+4].l[q].s(19,b[c+a.p([],[])]);break;case 53667:d=a.p(37,49);var n=b[c+a.p({},"x")],p=b[c+a.p(null,[])];b[c+d]=n[p];break;case 54165:n=b[c+a.p(void 0,void 0)];p=b[c+a.p(null,[])];var y=b[c+a.p([],{})];Reflect.set(n,p,y);break;case 10406:d=a.p(null,65);n=b[c+a.p([],void 0)];p=b[c+a.p(43,[])];b[c+d]=delete n[p];break;case 27840:d=a.p({},"z");var h=b[c+a.p(null, +{})];b[c+d]=h+b[c+a.p(void 0,"q")];break;case 934:d=a.p(void 0,"z");h=b[c+a.p(null,[])];b[c+d]=h-b[c+a.p("q","q")];break;case 22512:d=a.p("z",82);h=b[c+a.p(74,[])];b[c+d]=h*b[c+a.p("k",{})];break;case 38776:d=a.p([],[]);h=b[c+a.p("y","k")];b[c+d]=h/b[c+a.p(0,{})];break;case 16591:d=a.p({},null);h=b[c+a.p(void 0,{})];b[c+d]=h%b[c+a.p([],void 0)];break;case 22715:d=a.p([],null);h=b[c+a.p(66,null)];b[c+d]=Math.pow(h,b[c+a.p([],null)]);break;case 40046:d=a.p(null,[]);h=b[c+a.p("x","x")];b[c+d]=h&b[c+ +a.p(32,void 0)];break;case 16840:d=a.p({},void 0);h=b[c+a.p(72,[])];b[c+d]=h|b[c+a.p(54,80)];break;case 35019:d=a.p(63,[]);h=b[c+a.p({},[])];b[c+d]=h^b[c+a.p(void 0,{})];break;case 12158:d=a.p(null,[]);h=b[c+a.p("z",[])];b[c+d]=h<>b[c+a.p(void 0,void 0)];break;case 63201:d=a.p(null,[]);h=b[c+a.p(null,[])];b[c+d]=h>>>b[c+a.p({},5)];break;case 35824:d=a.p([],null);h=b[c+a.p([],{})];b[c+d]=hb[c+a.p(void 0,{})];break;case 44681:d=a.p(18,74);h=b[c+a.p({},null)];b[c+d]=h<=b[c+a.p(void 0,void 0)];break;case 41007:d=a.p(null,void 0);h=b[c+a.p({},"x")];b[c+d]=h>=b[c+a.p({},null)];break;case 34701:d=a.p(void 0,[]);h=b[c+a.p(null,null)];b[c+d]=h===b[c+a.p(void 0,void 0)];break;case 53970:d=a.p(32,null);h=b[c+a.p([],{})];b[c+d]=h!==b[c+a.p("k",[])];break;case 28535:d=a.p({},"y");h=b[c+a.p(73,{})];b[c+d]=h==b[c+a.p("z",{})];break;case 35900:d=a.p(86, +"k");h=b[c+a.p("y","z")];b[c+d]=h!=b[c+a.p(null,"k")];break;case 64062:d=a.p(null,22);h=b[c+a.p(void 0,{})];b[c+d]=h in b[c+a.p(87,"z")];break;case 10154:d=a.p("q",[]);n=b[c+a.p({},null)];b[c+d]=n instanceof b[c+a.p(void 0,"k")];break;case 54307:d=a.p(null,"z");b[c+d]=-b[c+a.p({},void 0)];break;case 13754:d=a.p({},4);b[c+d]=+b[c+a.p([],"q")];break;case 29330:d=a.p({},void 0);b[c+d]=!b[c+a.p("y","z")];break;case 54688:d=a.p(void 0,83);b[c+d]=~b[c+a.p("y",{})];break;case 63267:d=a.p(null,"q");b[c+d]= +typeof b[c+a.p(void 0,[])];break;case 38546:d=a.p([],45);a.p("k",null);b[c+d]=void 0;break;case 62836:d=a.p(void 0,78);var T=Q(a);y=Object.prototype.hasOwnProperty.call(a.k,T)?a.k[T]:void 0;b[c+d]=typeof y;break;case 60656:b[e+8]=a.p(5,null);break;case 22172:var K=a.p(null,[]),L=a.p(null,[]);b[c+K]||(b[e+8]=L);break;case 31454:K=a.p([],"z");L=a.p({},[]);b[c+K]&&(b[e+8]=L);break;case 33160:d=a.p([],void 0);var r=b[c+a.p(62,void 0)],t=a.p({},void 0);if(t===3208792843)g=b[c+a.p({},11)];else for(g=Array(t), +k=0;k>1):0]=F;break;case 42135:throw b[c+a.p(null,{})];case 1894:d=a.p({},"q");var ia=a.p({},{}),ja=a.p({},null),ka=a.p(null,void 0),W=a.p({},27),la=a.p(33,43),N=Array(W);for(k=0;k=I.B.length?b[e+8]=ra:b[c+d]=I.B[I.e++];break; +case 60149:var w=b[e+0];w||(b[e+0]=w=[]);w.push({Q:a.p("x",{}),C:a.p(null,null)});break;case 54011:b[e+0].pop();break;case 34588:(w=b[e+0])||(b[e+0]=w=[]);w.push({H:a.p(null,{}),I:a.p("x",{}),J:a.p("x",73),F:a.p({},[])});break;case 11569:var ca=a.p("y","z"),da=a.p([],[]),sa=a.p(null,"z"),J=a.p("z",void 0)^ca|0;for(e=da;e>>13)>>>0;break;case 36150:b[e+8]=b[c+a.p("q",73)];break;case 15003:debugger;break;default:throw Error("Unknown opcode: "+m+" at pc "+ +(b[e+8]-1));}}catch(z){e=0;for(l=a.h;l;){if((m=b[l+0])&&m.length>0){e=l;break}a.y("q",{},l,42);a.j=l;l=b[l+3];a.h=l}if(!e)throw z;l=b[e+0].pop();m=b[e+7];l.C!==void 0?(b[m+l.C]=z,b[e+8]=l.Q):(b[m+l.I]=l.F,b[m+l.J]=z,b[e+8]=l.H);a.j=e+b[e+9];a.h=e}if(!a.h)return a.g[0]}}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new B(null,["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],S,{},[56670,1,1894,5,89,0,4,1,0,1,2,44637,3,5,52197,5,0,44637,2,5,33160,5,3,0,44637,4,5,41170,5,0,0,5311,6,1,0,5311,7,2,0,33160,8,4,1,7,5311,7,3,0,33160,9,4,1,7,5311,7,4,0,33160,10,4,1,7,5311,7,5,0,33160,11,4,1,7,52197,7,5,8,9,10,11,2,54165,5,6,7,5311,5,6,0,33142,5,56670,1,5311,3,2,0,44637,2,3,1894,3,117,1,12,2,0,0,0,1,2,33142,3,5311,3,6,0,33142,3,56670, +2,34588,326,4,5,353,60149,264,3,56573,7,0,5311,8,7,0,53667,9,7,8,5311,8,8,0,27840,10,8,0,44094,8,7,9,1,10,5311,7,2,0,34701,8,0,7,22172,8,185,41170,7,9,0,5311,8,10,0,53667,9,7,8,5311,8,11,0,44094,10,7,9,1,8,5311,7,3,0,34701,8,0,7,22172,8,211,5311,7,12,0,44637,6,7,5311,7,13,0,53667,8,6,7,5311,7,4,0,34701,8,0,7,22172,8,237,41170,7,14,0,5311,8,15,0,6750,9,7,1,8,42135,9,56573,7,1,5311,8,3,0,27840,9,7,8,2347,1,9,54011,44637,5,9,29785,4,355,54011,60656,326,54011,60656,320,56573,7,0,5311,8,7,0,53667,9,7, +8,5311,8,16,0,5311,10,17,0,53667,11,3,10,27840,10,8,11,44094,8,7,9,1,10,56573,7,1,5311,8,3,0,27840,9,7,8,2347,1,9,44637,5,9,29785,4,357,54011,60656,326,54011,29785,4,359,60656,326,56573,7,0,5311,8,7,0,53667,9,7,8,5311,8,18,0,27840,10,8,0,44094,8,7,9,1,10,36150,4,42135,5,33142,5,33142,5,5311,7,6,0,33142,7]),new x({d:0,m:12,A:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-193.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-193.js new file mode 100644 index 00000000..79e99ce1 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes-minified-193.js @@ -0,0 +1,21 @@ +function w(e,a){this.r=a;this.I=e;this.w=[];this.prototype={}}function A(e,a){this.g=a;this.v=e;this.o=!1;this.u=void 0}function da(e){return typeof Buffer!=="undefined"?Buffer.from(e,"base64"):Uint8Array.from(atob(e),function(a){return a.charCodeAt(0)})}var B=new WeakMap;function C(e,a,g,d){this.j=d;this.F=g;this.t=a;this.i=e;this.A=null;this.g=[];this.l=6;this.h=0}C.prototype.c=function(e,a,g,d){e=this.g[d+6]+a;a=this.A||(this.A=[]);this.y=g;return a[e]||(a[e]=new A(e,this.g))}; +A.prototype.x=function(e,a,g){this.f=g;this.o?this.u=e:this.g[this.v]=e};C.prototype.d=function(){return this.j[this.g[this.h+2]++]};function D(e){var a=e.d([]),g=e.d(12);e=e.F[a];if(!g)return e;if(typeof e==="number")return e^g;if(typeof e!=="string")return e;e=da(e);a="";for(var d=0;d>>13)&65535);return a}function ea(e){return e.o?e.u:e.g[e.v]} +function P(e,a,g,d,f,k,b){for(var c=d.I,l=e.g,p=e.l,m=15+c.m,n=p+m;l.length=this.j.length)break;a[d+2]=k+1;k=this.j[k];a[d+7]++;try{var b=a[d+6];switch(k){case 48173:var c=this.d([]);a[b+c]=D(this);break;case 165:c=this.d(null);a[b+c]=this.d({});break;case 40683:c=this.d({});var l=D(this);if(!(l in this.i))throw new ReferenceError(`${l} is not defined`);a[b+c]=this.i[l];break;case 64726:c=this.d("z");a[b+c]=ea(a[d+11].w[this.d([])]);break;case 32591:c=this.d(null); +a[b+c]=a[d+0];break;case 54717:c=this.d(48);a[b+c]=a[b+this.d({})];break;case 4141:this.i[D(this)]=a[b+this.d(null)];break;case 54656:var p=this.d({});a[d+11].w[p].x(a[b+this.d(22)],{},44,[]);break;case 20296:c=this.d({});var m=a[b+this.d(void 0)],n=a[b+this.d(void 0)];a[b+c]=m[n];break;case 40621:m=a[b+this.d(51)];n=a[b+this.d(void 0)];var x=a[b+this.d(void 0)];Reflect.set(m,n,x);break;case 728:c=this.d(89);m=a[b+this.d(void 0)];n=a[b+this.d({})];a[b+c]=delete m[n];break;case 12927:c=this.d(void 0); +var h=a[b+this.d({})];a[b+c]=h+a[b+this.d([])];break;case 54142:c=this.d(27);h=a[b+this.d(86)];a[b+c]=h-a[b+this.d({})];break;case 28617:c=this.d([]);h=a[b+this.d("y")];a[b+c]=h*a[b+this.d([])];break;case 13717:c=this.d(null);h=a[b+this.d([])];a[b+c]=h/a[b+this.d(54)];break;case 12907:c=this.d(92);h=a[b+this.d({})];a[b+c]=h%a[b+this.d("z")];break;case 20742:c=this.d("q");h=a[b+this.d("k")];a[b+c]=Math.pow(h,a[b+this.d([])]);break;case 23382:c=this.d({});h=a[b+this.d("x")];a[b+c]=h&a[b+this.d({})]; +break;case 55577:c=this.d(51);h=a[b+this.d("y")];a[b+c]=h|a[b+this.d(void 0)];break;case 47611:c=this.d([]);h=a[b+this.d([])];a[b+c]=h^a[b+this.d(void 0)];break;case 32957:c=this.d("x");h=a[b+this.d(74)];a[b+c]=h<>a[b+this.d(void 0)];break;case 63987:c=this.d(void 0);h=a[b+this.d(79)];a[b+c]=h>>>a[b+this.d(null)];break;case 34619:c=this.d("y");h=a[b+this.d([])];a[b+c]=ha[b+this.d(80)];break;case 46904:c=this.d(49);h=a[b+this.d([])];a[b+c]=h<=a[b+this.d(26)];break;case 41281:c=this.d([]);h=a[b+this.d({})];a[b+c]=h>=a[b+this.d({})];break;case 28815:c=this.d(null);h=a[b+this.d({})];a[b+c]=h===a[b+this.d(void 0)];break;case 48755:c=this.d(void 0);h=a[b+this.d("q")];a[b+c]=h!==a[b+this.d(null)];break;case 63420:c=this.d({});h=a[b+this.d(null)];a[b+c]=h==a[b+this.d("k")];break;case 55099:c=this.d(void 0);h=a[b+this.d(null)];a[b+c]=h!=a[b+this.d([])]; +break;case 25677:c=this.d("q");h=a[b+this.d([])];a[b+c]=h in a[b+this.d(26)];break;case 46221:c=this.d([]);m=a[b+this.d("k")];a[b+c]=m instanceof a[b+this.d([])];break;case 60250:c=this.d(null);a[b+c]=-a[b+this.d([])];break;case 46577:c=this.d("z");a[b+c]=+a[b+this.d(null)];break;case 6263:c=this.d(void 0);a[b+c]=!a[b+this.d(44)];break;case 64253:c=this.d(null);a[b+c]=~a[b+this.d([])];break;case 43251:c=this.d([]);a[b+c]=typeof a[b+this.d("k")];break;case 20308:c=this.d([]);this.d([]);a[b+c]=void 0; +break;case 5179:c=this.d("y");var S=D(this);x=Object.prototype.hasOwnProperty.call(this.i,S)?this.i[S]:void 0;a[b+c]=typeof x;break;case 16135:a[d+2]=this.d("y");break;case 26982:var J=this.d([]),K=this.d([]);a[b+J]||(a[d+2]=K);break;case 36219:J=this.d(null);K=this.d({});a[b+J]&&(a[d+2]=K);break;case 63:c=this.d([]);var q=a[b+this.d({})],r=this.d("z");if(r===1330407607)g=a[b+this.d(83)];else for(g=Array(r),f=0;f>1):0]=E;break;case 25297:throw a[b+this.d([])];case 35054:c=this.d([]);var ha=this.d("x"),ia=this.d("y"),ja=this.d("q"),V=this.d({}),ka=this.d(null),M=Array(V);for(f=0;f=H.z.length?a[d+2]=qa:a[b+c]=H.z[H.b++];break;case 48497:var u=a[d+3];u||(a[d+3]=u=[]);u.push({H:this.d(null),C:this.d({})});break;case 5166:a[d+3].pop(); +break;case 3456:(u=a[d+3])||(a[d+3]=u=[]);u.push({K:this.d([]),E:this.d("k"),J:this.d([]),M:this.d(92)});break;case 27348:var ba=this.d({}),ca=this.d(void 0),ra=this.d(null),I=this.d({})^ba|0;for(d=ca;d>>13)>>>0;break;case 24842:a[d+2]=a[b+this.d({})];break;case 31048:debugger;break;default:throw Error("Unknown opcode: "+k+" at pc "+(a[d+2]-1));}}catch(y){d=0;for(k=this.h;k;){var v=a[k+3];if(v&&v.length>0){d=k;break}Q(this,k,{});this.l=k;this.h= +k=a[k+12]}if(!d)throw y;k=a[d+3].pop();v=a[d+6];k.C!==void 0?(a[v+k.C]=y,a[d+2]=k.H):(a[v+k.E]=k.M,a[v+k.J]=y,a[d+2]=k.K);this.l=d+a[d+8];this.h=d}if(!this.h)return this.g[0]}};var R=globalThis;typeof window!=="undefined"&&(R.window=window,R.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(R.module=module,R.exports=typeof exports!=="undefined"?exports:void 0); +(new C(R,[],["window","TEST_OUTPUT",0,1,2,3,void 0,"push","try:","JSON","parse","{",null,"missing","Error","explicit","catch:","name","finally:"],[32591,1,35054,5,89,0,4,1,0,1,2,54717,3,5,13063,5,0,54717,2,5,63,5,3,0,54717,4,5,40683,5,0,0,48173,6,1,0,48173,7,2,0,63,8,4,1,7,48173,7,3,0,63,9,4,1,7,48173,7,4,0,63,10,4,1,7,48173,7,5,0,63,11,4,1,7,13063,7,5,8,9,10,11,2,40621,5,6,7,48173,5,6,0,39588,5,32591,1,48173,3,2,0,54717,2,3,35054,3,117,1,12,2,0,0,0,1,2,39588,3,48173,3,6,0,39588,3,32591,2,3456,326, +4,5,353,48497,264,3,64726,7,0,48173,8,7,0,20296,9,7,8,48173,8,8,0,12927,10,8,0,48692,8,7,9,1,10,48173,7,2,0,28815,8,0,7,26982,8,185,40683,7,9,0,48173,8,10,0,20296,9,7,8,48173,8,11,0,48692,10,7,9,1,8,48173,7,3,0,28815,8,0,7,26982,8,211,48173,7,12,0,54717,6,7,48173,7,13,0,20296,8,6,7,48173,7,4,0,28815,8,0,7,26982,8,237,40683,7,14,0,48173,8,15,0,13891,9,7,1,8,25297,9,64726,7,1,48173,8,3,0,12927,9,7,8,54656,1,9,5166,54717,5,9,165,4,355,5166,16135,326,5166,16135,320,64726,7,0,48173,8,7,0,20296,9,7,8,48173, +8,16,0,48173,10,17,0,20296,11,3,10,12927,10,8,11,48692,8,7,9,1,10,64726,7,1,48173,8,3,0,12927,9,7,8,54656,1,9,54717,5,9,165,4,357,5166,16135,326,5166,165,4,359,16135,326,64726,7,0,48173,8,7,0,20296,9,7,8,48173,8,18,0,12927,10,8,0,48692,8,7,9,1,10,24842,4,25297,5,39588,5,39588,5,48173,7,6,0,39588,7])).s(new w({Q:0,m:12,D:0},"z"),void 0,null,"q"); diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes.baseline.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes.baseline.js new file mode 100644 index 00000000..e72aa743 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes.baseline.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */0, /* 3 */1, /* 4 */2, /* 5 */3, /* 6 */undefined, /* 7 */"push", /* 8 */"try:", /* 9 */"JSON", /* 10 */"parse", /* 11 */"{", /* 12 */null, /* 13 */"missing", /* 14 */"Error", /* 15 */"explicit", /* 16 */"catch:", /* 17 */"name", /* 18 */"finally:"]; +var BYTECODE = [4, 1, 47, 5, 89, 0, 4, 1, 0, 1, 2, 5, 3, 5, 48, 5, 0, 5, 2, 5, 42, 5, 3, 0, 5, 4, 5, 2, 5, 0, 0, 0, 6, 1, 0, 0, 7, 2, 0, 42, 8, 4, 1, 7, 0, 7, 3, 0, 42, 9, 4, 1, 7, 0, 7, 4, 0, 42, 10, 4, 1, 7, 0, 7, 5, 0, 42, 11, 4, 1, 7, 48, 7, 5, 8, 9, 10, 11, 2, 9, 5, 6, 7, 0, 5, 6, 0, 45, 5, 4, 1, 0, 3, 2, 0, 5, 2, 3, 47, 3, 117, 1, 12, 2, 0, 0, 0, 1, 2, 45, 3, 0, 3, 6, 0, 45, 3, 4, 2, 59, 326, 4, 5, 353, 54, 264, 3, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 8, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 0, 7, 2, 0, 26, 8, 0, 7, 40, 8, 185, 2, 7, 9, 0, 0, 8, 10, 0, 8, 9, 7, 8, 0, 8, 11, 0, 43, 10, 7, 9, 1, 8, 0, 7, 3, 0, 26, 8, 0, 7, 40, 8, 211, 0, 7, 12, 0, 5, 6, 7, 0, 7, 13, 0, 8, 8, 6, 7, 0, 7, 4, 0, 26, 8, 0, 7, 40, 8, 237, 2, 7, 14, 0, 0, 8, 15, 0, 44, 9, 7, 1, 8, 46, 9, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 55, 5, 5, 9, 1, 4, 355, 55, 39, 326, 55, 39, 320, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 16, 0, 0, 10, 17, 0, 8, 11, 3, 10, 11, 10, 8, 11, 43, 8, 7, 9, 1, 10, 3, 7, 1, 0, 8, 3, 0, 11, 9, 7, 8, 7, 1, 9, 5, 5, 9, 1, 4, 357, 55, 39, 326, 55, 1, 4, 359, 39, 326, 3, 7, 0, 0, 8, 7, 0, 8, 9, 7, 8, 0, 8, 18, 0, 11, 10, 8, 0, 43, 8, 7, 9, 1, 10, 58, 4, 46, 5, 45, 5, 45, 5, 0, 7, 6, 0, 45, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes.source.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes.source.js new file mode 100644 index 00000000..7b4d094e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/exceptionRoutes.source.js @@ -0,0 +1 @@ +const trace = []; function make() { let count = 0; return function run(mode) { try { trace.push("try:" + mode); if (mode === 0) JSON.parse("{"); if (mode === 1) { const value = null; value.missing; } if (mode === 2) throw new Error("explicit"); return ++count; } catch (error) { trace.push("catch:" + error.name); return ++count; } finally { trace.push("finally:" + mode); } }; } const run = make(); window.TEST_OUTPUT = [run(0), run(1), run(2), run(3), trace]; diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/provenance.json b/test/vm/jsconfuser-vm/fixtures/randomized-class/provenance.json new file mode 100644 index 00000000..e45c53e3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/provenance.json @@ -0,0 +1,1672 @@ +{ + "schemaVersion": "randomized-class-fixtures.v1", + "encoderCommit": "20c5b96bf57337c568579352758d7685358650de", + "fixtureRows": [ + { + "path": "arithmetic-181.js", + "bytes": 35220, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "arithmetic--randomizeOpcodesClassObfuscation--181", + "source": { + "bytes": 38 + }, + "emission": { + "bytes": 35220 + }, + "rawEmission": { + "bytes": 36292, + "sha256": "0df5daaa36f426eb6e6cb124ad59a8fb496075cfa79b7f9b44a209d24f91a34d" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 608, + "seed": 181, + "state": 1195484821 + }, + "generationJournal": { + "bytes": 277451, + "sha256": "d320160b3e367b09212b0c07b4fd28b108bafb00716d944c52b752d721fa0f09" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 582536, + "sha256": "814a5b1328bdcb24545d2962ac46a241594da9c06d2e4f237e03641844553e48" + } + } + }, + { + "path": "arithmetic-182.js", + "bytes": 36614, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "arithmetic--randomizeOpcodesClassObfuscation--182", + "source": { + "bytes": 38 + }, + "emission": { + "bytes": 36614 + }, + "rawEmission": { + "bytes": 37686, + "sha256": "bdfdb1d6003b8335683b3944c9f423f9f68469abec430a3761233656fb2b6c49" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 943, + "seed": 182, + "state": 589708849 + }, + "generationJournal": { + "bytes": 428784, + "sha256": "1186ed9c8aa2f02f68946855cef2c39c947d6be4310c8c0ad4990724d34f1234" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 757860, + "sha256": "d246c0b5d824c5efcf47361143694dfd2e32e2bcf7165b9f4ffda43b5fbdd8f8" + } + } + }, + { + "path": "arithmetic-183.js", + "bytes": 37815, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "arithmetic--randomizeOpcodesClassObfuscation--183", + "source": { + "bytes": 38 + }, + "emission": { + "bytes": 37815 + }, + "rawEmission": { + "bytes": 38887, + "sha256": "89cac0d0c8a0e8a3a69968a8399486460d6ae68059e3c15c5563033f6c6c98c7" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 1228, + "seed": 183, + "state": 2894922739 + }, + "generationJournal": { + "bytes": 557765, + "sha256": "d5bf96e839d0c6edbb08c5e13c6e36c3dc9dee914e177fd386adc3b1bb6bfccc" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 906864, + "sha256": "b72689454d062e789d3823f82e7170c73c2757e3ad047cfe9b4b31a3d0392e59" + } + } + }, + { + "path": "arithmetic-minified-191.js", + "bytes": 7980, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "de9ed90b93099dae69c38451c2d1e912bb3b98538a551fa37ff3cffa268db572", + "producer": { + "id": "arithmetic--randomizeOpcodesClassObfuscationMinify--191", + "source": { + "bytes": 38 + }, + "emission": { + "bytes": 7980 + }, + "rawEmission": { + "bytes": 7980 + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 575, + "seed": 191, + "state": 883355146 + }, + "generationJournal": { + "bytes": 292712, + "sha256": "301a897f400358a4339647757155915c1521d2f708db70204023620965967d76" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [ + { + "executable": "java", + "argv": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "--sun-misc-unsafe-memory-access=allow", + "-jar", + "node_modules/google-closure-compiler-java/compiler.jar", + "--compilation_level=ADVANCED", + "--warning_level=QUIET" + ], + "startedAt": "2026-10-06T08:11:25.332Z", + "completedAt": "2026-10-06T08:11:26.382Z", + "exitCode": 0, + "signal": null, + "timedOut": false, + "stderrSha256": "9717649348925c2b6905ad51a04ee2aab3d9a10eee90372c56f53b0425423c5b" + } + ], + "result": { + "bytes": 13917558, + "sha256": "63f8bb3f6fbbb1b5e1ed9f580ef12fd588287c7d7daf10d7e1d80c13fba83cbe" + } + } + }, + { + "path": "arithmetic-minified-192.js", + "bytes": 8758, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "de9ed90b93099dae69c38451c2d1e912bb3b98538a551fa37ff3cffa268db572", + "producer": { + "id": "arithmetic--randomizeOpcodesClassObfuscationMinify--192", + "source": { + "bytes": 38 + }, + "emission": { + "bytes": 8758 + }, + "rawEmission": { + "bytes": 8758 + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 800, + "seed": 192, + "state": 668802656 + }, + "generationJournal": { + "bytes": 397090, + "sha256": "f7c3aee7319282f1b514b90d5a82c2a07536cfd09e9dd5a1478837b9a80b6c22" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [ + { + "executable": "java", + "argv": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "--sun-misc-unsafe-memory-access=allow", + "-jar", + "node_modules/google-closure-compiler-java/compiler.jar", + "--compilation_level=ADVANCED", + "--warning_level=QUIET" + ], + "startedAt": "2026-10-06T08:11:27.979Z", + "completedAt": "2026-10-06T08:11:29.054Z", + "exitCode": 0, + "signal": null, + "timedOut": false, + "stderrSha256": "9717649348925c2b6905ad51a04ee2aab3d9a10eee90372c56f53b0425423c5b" + } + ], + "result": { + "bytes": 14037540, + "sha256": "457edadc600deb87eeb09308515069949b0f26b0bf9b56848e639b3428a65014" + } + } + }, + { + "path": "arithmetic-minified-193.js", + "bytes": 8678, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "de9ed90b93099dae69c38451c2d1e912bb3b98538a551fa37ff3cffa268db572", + "producer": { + "id": "arithmetic--randomizeOpcodesClassObfuscationMinify--193", + "source": { + "bytes": 38 + }, + "emission": { + "bytes": 8678 + }, + "rawEmission": { + "bytes": 8678 + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 579, + "seed": 193, + "state": 3914651104 + }, + "generationJournal": { + "bytes": 296596, + "sha256": "7738052f6a049c61ffa25d600e3de2faa59218d1d1d46057f46141fca6fa471c" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [ + { + "executable": "java", + "argv": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "--sun-misc-unsafe-memory-access=allow", + "-jar", + "node_modules/google-closure-compiler-java/compiler.jar", + "--compilation_level=ADVANCED", + "--warning_level=QUIET" + ], + "startedAt": "2026-10-06T08:11:30.694Z", + "completedAt": "2026-10-06T08:11:31.754Z", + "exitCode": 0, + "signal": null, + "timedOut": false, + "stderrSha256": "9717649348925c2b6905ad51a04ee2aab3d9a10eee90372c56f53b0425423c5b" + } + ], + "result": { + "bytes": 13925672, + "sha256": "3681ad395a47c9f4121839b369f4fd25664bbb232cb52207f36f1d0793b902cf" + } + } + }, + { + "path": "arithmetic.baseline.js", + "bytes": 37814, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "arithmetic--baseline--181", + "source": { + "bytes": 38 + }, + "emission": { + "bytes": 37814 + }, + "rawEmission": { + "bytes": 38886, + "sha256": "459a9c4df35479fbeb480f1ef4ef9d21c68c488182a67c2ccc366a32c4405324" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 181, + "state": 181 + }, + "generationJournal": { + "bytes": 2889, + "sha256": "c9312e34f9abed2b152e645a330d2a55978ce08e6dafee6029718c13c80e374e" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 13700964, + "sha256": "c648dc861161565da91a4b219f1793b0e8b9683b3c4f6ce55a20d5f58b18dc64" + } + } + }, + { + "path": "arithmetic.source.js", + "bytes": 38, + "producerFile": "source.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "arithmetic--baseline--181", + "source": { + "bytes": 38 + }, + "emission": { + "bytes": 37814 + }, + "rawEmission": { + "bytes": 38886, + "sha256": "459a9c4df35479fbeb480f1ef4ef9d21c68c488182a67c2ccc366a32c4405324" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 181, + "state": 181 + }, + "generationJournal": { + "bytes": 2889, + "sha256": "c9312e34f9abed2b152e645a330d2a55978ce08e6dafee6029718c13c80e374e" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 13700964, + "sha256": "c648dc861161565da91a4b219f1793b0e8b9683b3c4f6ce55a20d5f58b18dc64" + } + } + }, + { + "path": "exceptionRoutes-181.js", + "bytes": 36770, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "exceptionRoutes--randomizeOpcodesClassObfuscation--181", + "source": { + "bytes": 462 + }, + "emission": { + "bytes": 36770 + }, + "rawEmission": { + "bytes": 45675, + "sha256": "e6cc570349ffa27e62e3d61b0e6d71b73ccbab89f30986fc623fb23d2143a19c" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 608, + "seed": 181, + "state": 1195484821 + }, + "generationJournal": { + "bytes": 277451, + "sha256": "241c55d11c6999f9be8be009d4848ae74587eb2fbece4a9a0db858b994634dd7" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 630004, + "sha256": "0bd28b04c61795b0e2ca5163936a2fd95082374b016b3413ddad13e3fef31c8b" + } + } + }, + { + "path": "exceptionRoutes-182.js", + "bytes": 38164, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "exceptionRoutes--randomizeOpcodesClassObfuscation--182", + "source": { + "bytes": 462 + }, + "emission": { + "bytes": 38164 + }, + "rawEmission": { + "bytes": 47071, + "sha256": "224d09da00b901d6704c64fae365f0fc34df38066a7d91868f65ba3c180e107c" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 943, + "seed": 182, + "state": 589708849 + }, + "generationJournal": { + "bytes": 428784, + "sha256": "b6cd592bdf44393759cf39d718a2ca3dc3c025b30a119317f2121ac27d37966f" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 805332, + "sha256": "d144af84bcc9d4a7ac40c2fd58921a93b53446cc006aeecb0a11cb8407f879cb" + } + } + }, + { + "path": "exceptionRoutes-183.js", + "bytes": 39335, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "exceptionRoutes--randomizeOpcodesClassObfuscation--183", + "source": { + "bytes": 462 + }, + "emission": { + "bytes": 39335 + }, + "rawEmission": { + "bytes": 48238, + "sha256": "df5827c2b00b37a1ab1916b3bc3a26d65ba2636a9e7162a498db54899b3c8532" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 1228, + "seed": 183, + "state": 2894922739 + }, + "generationJournal": { + "bytes": 557765, + "sha256": "3fc6c8fc511b5e968639d7be99c9c96b85f189fd35456880a766f341af51468d" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 954148, + "sha256": "abf58c4aac5b6513266f1417ef106a4024f2f62ba5b817bc5cd5ccc1b83c45c2" + } + } + }, + { + "path": "exceptionRoutes-minified-191.js", + "bytes": 9094, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "de9ed90b93099dae69c38451c2d1e912bb3b98538a551fa37ff3cffa268db572", + "producer": { + "id": "exceptionRoutes--randomizeOpcodesClassObfuscationMinify--191", + "source": { + "bytes": 462 + }, + "emission": { + "bytes": 9094 + }, + "rawEmission": { + "bytes": 9094 + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 575, + "seed": 191, + "state": 883355146 + }, + "generationJournal": { + "bytes": 296319, + "sha256": "ab438d86e0ff3493ca5ab14540a65a123eb1b86371bfcacede47ec004af3b06f" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [ + { + "executable": "java", + "argv": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "--sun-misc-unsafe-memory-access=allow", + "-jar", + "node_modules/google-closure-compiler-java/compiler.jar", + "--compilation_level=ADVANCED", + "--warning_level=QUIET" + ], + "startedAt": "2026-10-06T08:13:17.104Z", + "completedAt": "2026-10-06T08:13:18.183Z", + "exitCode": 0, + "signal": null, + "timedOut": false, + "stderrSha256": "9717649348925c2b6905ad51a04ee2aab3d9a10eee90372c56f53b0425423c5b" + } + ], + "result": { + "bytes": 132583504, + "sha256": "46bfbf30853a5dac8babbc2cff9f1ea3567bf2ae1c1bd568c41bbcd821c9913e" + } + } + }, + { + "path": "exceptionRoutes-minified-192.js", + "bytes": 9870, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "de9ed90b93099dae69c38451c2d1e912bb3b98538a551fa37ff3cffa268db572", + "producer": { + "id": "exceptionRoutes--randomizeOpcodesClassObfuscationMinify--192", + "source": { + "bytes": 462 + }, + "emission": { + "bytes": 9870 + }, + "rawEmission": { + "bytes": 9870 + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 800, + "seed": 192, + "state": 668802656 + }, + "generationJournal": { + "bytes": 400498, + "sha256": "0507698f115e7790ae06294593aa9a1a405fcf5d33d6366116ae07bc53e2649d" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [ + { + "executable": "java", + "argv": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "--sun-misc-unsafe-memory-access=allow", + "-jar", + "node_modules/google-closure-compiler-java/compiler.jar", + "--compilation_level=ADVANCED", + "--warning_level=QUIET" + ], + "startedAt": "2026-10-06T08:13:20.215Z", + "completedAt": "2026-10-06T08:13:21.295Z", + "exitCode": 0, + "signal": null, + "timedOut": false, + "stderrSha256": "9717649348925c2b6905ad51a04ee2aab3d9a10eee90372c56f53b0425423c5b" + } + ], + "result": { + "bytes": 132703250, + "sha256": "c733a07a08d3d07d6c02ed6830be91446d463f86edebe2be5455de4a70a452a2" + } + } + }, + { + "path": "exceptionRoutes-minified-193.js", + "bytes": 9788, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "de9ed90b93099dae69c38451c2d1e912bb3b98538a551fa37ff3cffa268db572", + "producer": { + "id": "exceptionRoutes--randomizeOpcodesClassObfuscationMinify--193", + "source": { + "bytes": 462 + }, + "emission": { + "bytes": 9788 + }, + "rawEmission": { + "bytes": 9788 + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 579, + "seed": 193, + "state": 3914651104 + }, + "generationJournal": { + "bytes": 300191, + "sha256": "2c7bd6ee1c96e5826c88a457b7ddb960a9b23d60430e8d414afc31b26e82b4be" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [ + { + "executable": "java", + "argv": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "--sun-misc-unsafe-memory-access=allow", + "-jar", + "node_modules/google-closure-compiler-java/compiler.jar", + "--compilation_level=ADVANCED", + "--warning_level=QUIET" + ], + "startedAt": "2026-10-06T08:13:23.355Z", + "completedAt": "2026-10-06T08:13:24.414Z", + "exitCode": 0, + "signal": null, + "timedOut": false, + "stderrSha256": "9717649348925c2b6905ad51a04ee2aab3d9a10eee90372c56f53b0425423c5b" + } + ], + "result": { + "bytes": 132591582, + "sha256": "cad721f43a5751b5b79049e1b7d2075cd07ddc0d4f29abc775f73e9333c3eb29" + } + } + }, + { + "path": "exceptionRoutes.baseline.js", + "bytes": 39060, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "exceptionRoutes--baseline--181", + "source": { + "bytes": 462 + }, + "emission": { + "bytes": 39060 + }, + "rawEmission": { + "bytes": 47941, + "sha256": "91e18a17e38173072e102ea34e9561b10f16805560aff6edc3bcf5889d4ef3e9" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 181, + "state": 181 + }, + "generationJournal": { + "bytes": 2889, + "sha256": "e0073c7846240251101a1329757d7f8a41ac21abcd9bb40c61de7f90a1844845" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 132377340, + "sha256": "2051a5e9bc511a3bb25307d87e9c468edc69301e5ec55c0a854f81899fb9fecb" + } + } + }, + { + "path": "exceptionRoutes.source.js", + "bytes": 462, + "producerFile": "source.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "exceptionRoutes--baseline--181", + "source": { + "bytes": 462 + }, + "emission": { + "bytes": 39060 + }, + "rawEmission": { + "bytes": 47941, + "sha256": "91e18a17e38173072e102ea34e9561b10f16805560aff6edc3bcf5889d4ef3e9" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 181, + "state": 181 + }, + "generationJournal": { + "bytes": 2889, + "sha256": "e0073c7846240251101a1329757d7f8a41ac21abcd9bb40c61de7f90a1844845" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 132377340, + "sha256": "2051a5e9bc511a3bb25307d87e9c468edc69301e5ec55c0a854f81899fb9fecb" + } + } + }, + { + "path": "spread-181.js", + "bytes": 35363, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "spread--randomizeOpcodesClassObfuscation--181", + "source": { + "bytes": 93 + }, + "emission": { + "bytes": 35363 + }, + "rawEmission": { + "bytes": 37229, + "sha256": "2d6270275f482ff060a85347a563ec30139de58790c3af458f3ab38ac0da62f3" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 608, + "seed": 181, + "state": 1195484821 + }, + "generationJournal": { + "bytes": 277451, + "sha256": "5c156415b19bdd04617201f91b412b130951932d5ec1c0d89be6748736ee9c29" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 585036, + "sha256": "bf3c142afe5daceff1eafba63c871141c68df78743364082a4fae77d8302d148" + } + } + }, + { + "path": "spread-182.js", + "bytes": 36759, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "spread--randomizeOpcodesClassObfuscation--182", + "source": { + "bytes": 93 + }, + "emission": { + "bytes": 36759 + }, + "rawEmission": { + "bytes": 38626, + "sha256": "36b3a705b608f66aff04ce8730b4a37b67859db50ee1bbdd5a52c411a54d085a" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 943, + "seed": 182, + "state": 589708849 + }, + "generationJournal": { + "bytes": 428784, + "sha256": "a3269955c86a86abd360636fc26e68b505b3e3f6a3a45ed0ef223cafa3eda4b7" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 760374, + "sha256": "27e36584f5ee30d6180e1cd04a264435fda28d6b0a470ca7942b7c1aef0d3832" + } + } + }, + { + "path": "spread-183.js", + "bytes": 37956, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "spread--randomizeOpcodesClassObfuscation--183", + "source": { + "bytes": 93 + }, + "emission": { + "bytes": 37956 + }, + "rawEmission": { + "bytes": 39822, + "sha256": "befd05eb0088858ff7fd0880a2029ec14385d94f797aebc5c517d75b64621899" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 1228, + "seed": 183, + "state": 2894922739 + }, + "generationJournal": { + "bytes": 557765, + "sha256": "2b6e8bfb95a85e8d9dea5f49d8e30b23a470b928a427165352ae640cfae4290a" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 909352, + "sha256": "404ff665455c85d0630eb8ba0d361ed665d0d0b3d4b1d3222b1805d31a8e86f0" + } + } + }, + { + "path": "spread-minified-191.js", + "bytes": 8100, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "de9ed90b93099dae69c38451c2d1e912bb3b98538a551fa37ff3cffa268db572", + "producer": { + "id": "spread--randomizeOpcodesClassObfuscationMinify--191", + "source": { + "bytes": 93 + }, + "emission": { + "bytes": 8100 + }, + "rawEmission": { + "bytes": 8100 + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 575, + "seed": 191, + "state": 883355146 + }, + "generationJournal": { + "bytes": 293078, + "sha256": "b8c78fd08bc02725251ec75e981db48911007f8b48e6f31ac17ec2f37d090dac" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [ + { + "executable": "java", + "argv": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "--sun-misc-unsafe-memory-access=allow", + "-jar", + "node_modules/google-closure-compiler-java/compiler.jar", + "--compilation_level=ADVANCED", + "--warning_level=QUIET" + ], + "startedAt": "2026-10-06T08:12:17.438Z", + "completedAt": "2026-10-06T08:12:18.497Z", + "exitCode": 0, + "signal": null, + "timedOut": false, + "stderrSha256": "9717649348925c2b6905ad51a04ee2aab3d9a10eee90372c56f53b0425423c5b" + } + ], + "result": { + "bytes": 23886035, + "sha256": "95a7ac2987483674f5a3ffea3e877f27439b06dcaf9a77d15eab43324c375376" + } + } + }, + { + "path": "spread-minified-192.js", + "bytes": 8876, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "de9ed90b93099dae69c38451c2d1e912bb3b98538a551fa37ff3cffa268db572", + "producer": { + "id": "spread--randomizeOpcodesClassObfuscationMinify--192", + "source": { + "bytes": 93 + }, + "emission": { + "bytes": 8876 + }, + "rawEmission": { + "bytes": 8876 + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 800, + "seed": 192, + "state": 668802656 + }, + "generationJournal": { + "bytes": 397450, + "sha256": "a7a8c59c9419108a3df962b9e23d772e5b8c185256d76f046113a2f6a15f6478" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [ + { + "executable": "java", + "argv": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "--sun-misc-unsafe-memory-access=allow", + "-jar", + "node_modules/google-closure-compiler-java/compiler.jar", + "--compilation_level=ADVANCED", + "--warning_level=QUIET" + ], + "startedAt": "2026-10-06T08:12:20.146Z", + "completedAt": "2026-10-06T08:12:21.224Z", + "exitCode": 0, + "signal": null, + "timedOut": false, + "stderrSha256": "9717649348925c2b6905ad51a04ee2aab3d9a10eee90372c56f53b0425423c5b" + } + ], + "result": { + "bytes": 24005999, + "sha256": "a307a0a499b5437f9a3e9b6fcecbf6f23fdcea57a18d67b6636a6e26cfecdeb8" + } + } + }, + { + "path": "spread-minified-193.js", + "bytes": 8794, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "de9ed90b93099dae69c38451c2d1e912bb3b98538a551fa37ff3cffa268db572", + "producer": { + "id": "spread--randomizeOpcodesClassObfuscationMinify--193", + "source": { + "bytes": 93 + }, + "emission": { + "bytes": 8794 + }, + "rawEmission": { + "bytes": 8794 + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": true, + "randomizeOpcodes": true, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 579, + "seed": 193, + "state": 3914651104 + }, + "generationJournal": { + "bytes": 296950, + "sha256": "2b915c72719dd91eca72f89339ab81beb76fdc142526d4fcb9e0a4667292fd1f" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [ + { + "executable": "java", + "argv": [ + "-XX:+IgnoreUnrecognizedVMOptions", + "--sun-misc-unsafe-memory-access=allow", + "-jar", + "node_modules/google-closure-compiler-java/compiler.jar", + "--compilation_level=ADVANCED", + "--warning_level=QUIET" + ], + "startedAt": "2026-10-06T08:12:22.909Z", + "completedAt": "2026-10-06T08:12:23.966Z", + "exitCode": 0, + "signal": null, + "timedOut": false, + "stderrSha256": "9717649348925c2b6905ad51a04ee2aab3d9a10eee90372c56f53b0425423c5b" + } + ], + "result": { + "bytes": 23894113, + "sha256": "3643fc98434bc48eae050ec89f720cf6be869c39958f86647c4db7588da996c2" + } + } + }, + { + "path": "spread.baseline.js", + "bytes": 37923, + "producerFile": "encoded.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "spread--baseline--181", + "source": { + "bytes": 93 + }, + "emission": { + "bytes": 37923 + }, + "rawEmission": { + "bytes": 39776, + "sha256": "7d5e1ad53a1730b63a200fb4dd4a246fe4c795f2eff3e4a19af4b530485350ac" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 181, + "state": 181 + }, + "generationJournal": { + "bytes": 2889, + "sha256": "1af6bff47a6da3384c8b46f0f0c2879f9f6a8405bb7d4a69695f4652a2ab6390" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 23670371, + "sha256": "0ca8a7fb3d55a632116cac9d2a677b11df8504f48649ee901ef268c747562e5c" + } + } + }, + { + "path": "spread.source.js", + "bytes": 93, + "producerFile": "source.js", + "producerPopulationSummarySha256": "5c3c2b9bbfe0fdef0819763a531ab0d832550ef5cea88eb2a472de1aa25c266c", + "producer": { + "id": "spread--baseline--181", + "source": { + "bytes": 93 + }, + "emission": { + "bytes": 37923 + }, + "rawEmission": { + "bytes": 39776, + "sha256": "7d5e1ad53a1730b63a200fb4dd4a246fe4c795f2eff3e4a19af4b530485350ac" + }, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + }, + "rng": { + "algorithm": "mulberry32", + "draws": 0, + "seed": 181, + "state": 181 + }, + "generationJournal": { + "bytes": 2889, + "sha256": "1af6bff47a6da3384c8b46f0f0c2879f9f6a8405bb7d4a69695f4652a2ab6390" + }, + "generationIdentities": { + "jar": { + "sha256": "4e187cea76222f444a74b1b336580281034789b2126af3e008c29106837fad6f" + }, + "java": { + "executable": "java", + "release": "IMPLEMENTOR=\"Homebrew\"\nIMPLEMENTOR_VERSION=\"Homebrew\"\nJAVA_RUNTIME_VERSION=\"27\"\nJAVA_VERSION=\"27\"\nJAVA_VERSION_DATE=\"2026-09-15\"\nLIBC=\"default\"\nMODULES=\"java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.md jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom\"\nOS_ARCH=\"aarch64\"\nOS_NAME=\"Darwin\"\nSOURCE=\"\"\n", + "sha256": "05687a76275264affc701d1cbc005960002295e2da9f2833aa6c0ebfecd77943" + } + }, + "processes": [], + "result": { + "bytes": 23670371, + "sha256": "0ca8a7fb3d55a632116cac9d2a677b11df8504f48649ee901ef268c747562e5c" + } + } + } + ], + "boundary": "Exact source/options/RNG/emission provenance; finite recognition fixtures, no general semantics claim" +} diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-181.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-181.js new file mode 100644 index 00000000..92767dd3 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-181.js @@ -0,0 +1,960 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(fake_14, bytecode, constants, fake_13, fake_15, globals) { + this.v = bytecode; + this.X = constants; + this.ac = fake_14; + this.Y = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.N = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.f = []; + this.R = 11; + this.F = fake_15 || fake_13; + this.P = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +function Upvalue(absSlot, fake_3, regs, fake_2, fake_1) { + this.n = fake_3; + this.f = regs; // shared reference to VM._regs flat array + this.r = fake_2; + this.i = absSlot; // absolute index; stable as long as frame is alive + this.J = false; + this.o = undefined; + this.M = fake_1; +} +var BYTECODE = [2151, 1, 18527, 4, 68, 2, 5, 0, 0, 21164, 2, 4, 26188, 4, 0, 0, 26188, 5, 1, 0, 28412, 6, 2, 4, 5, 21164, 3, 6, 56464, 4, 2, 0, 26188, 5, 3, 0, 28412, 6, 0, 26188, 7, 4, 0, 63110, 8, 6, 7, 15913, 7, 6, 8, 1, 3, 4545, 6, 2, 3847166315, 7, 45975, 4, 5, 6, 26188, 4, 5, 0, 62456, 4, 2151, 3, 37279, 4, 0, 1, 62456, 4, 26188, 4, 5, 0, 62456, 4]; +// Closure & Frame +function Closure(fake_12, fake_11, fn) { + this.l = fn; + this.K = fake_12; + this.C = []; + this.prototype = {}; // <- default prototype object for `new` + this.s = fake_11; +} +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CONSTANTS = [/* 0 */2, /* 1 */5, /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */"concat", /* 5 */undefined]; +VM.prototype.aa = function (fake_20, fp, fake_23, fake_22, slot, fake_21) { + this.p = fake_21 || fake_22; + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.f[fp + 7] + slot; + var uvs = this.N || (this.N = []); + this.G = fake_23; + this.Z = fake_20; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, "y", this.f, {}, [])); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.E = function (idxIn, fake_25, fake_24, keyIn) { + this.w = [fake_24, fake_25]; + var idx = idxIn ?? this.S(undefined); + var key = keyIn ?? this.S({}); + var v = this.X[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +Upvalue.prototype.O = function (fake_6, v, fake_7) { + if (this.J) this.o = v;else this.f[this.i] = v; + this.j = fake_7; + this.H = fake_6; +}; +Upvalue.prototype.u = function (fake_10, fake_8, fake_9) { + this.af = fake_10; + this.o = this.f[this.i]; + this.J = true; + this.h = fake_9 || fake_8; +}; +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.ag = function (fake_17, fake_18, fake_19, thisVal, retDst, args, closure) { + var fn = closure.l; + var regs = this.f; + this.W = fake_17; + var fp = this.R; + var size = 13 + fn.q; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 13; + regs[fp + 1] = fn.ab; + regs[fp + 10] = this.P; + this.e = fake_18; + regs[fp + 8] = retDst; + regs[fp + 2] = thisVal; + regs[fp + 3] = closure; + regs[fp + 4] = size; + regs[fp + 7] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof 9 === "number") regs[fp + 9] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.U; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.ab; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.d = fake_19; + this.R = end; + if (args) { + if (fn.z) { + var restSlot = fn.U - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.q; a++) regs[base + a] = args[a]; + } + if (fn.U < fn.q) regs[base + fn.U] = args; + } + this.P = fp; +}; +VM.prototype.S = function (fake_16) { + this.k = fake_16; + return this.v[this.f[this.P + 1]++]; +}; +VM.prototype.b = function (fake_27, fake_28, fake_29, fake_26, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.N; + this.Q = fake_27; + if (!uvs) return; // nothing in this program ever captured a local + this.x = fake_29 && fake_26; + var regs = this.f; + this.g = fake_28; + var hi = fp + regs[fp + 4]; + for (var i = regs[fp + 7]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.u("q", null, undefined); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.ah = function (fake_4, fake_5) { + this.T = [fake_5, fake_4]; + return this.J ? this.o : this.f[this.i]; +}; +VM.prototype.t = function (closure, args, fake_31, thisVal, fake_30, fake_32) { + this.A = fake_30 || fake_31; + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.ag(null, null, [], thisVal, 0, args, closure); + this.a = fake_32; + while (true) { + var fp = this.P; + var regs = this.f; + var bc = this.v; + var pc = regs[fp + 1]; + if (pc >= bc.length) break; + regs[fp + 1] = pc + 1; + var op = this.v[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof 0 === "number") regs[fp + 0] = regs[fp + 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.v.length; i++) this.v[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.R; i2++) this.f[i2] = undefined; + op = 39919; + regs[fp + 1] = this.v.length; // jump past end to halt + } + } + try { + var base = regs[fp + 7]; + + /* @SWITCH */ + switch (op) { + case 26188: + { + var dst = this.S([]); + regs[base + dst] = this.E(); + break; + } + case 35667: + { + var dst = this.S(74); + regs[base + dst] = this.S([]); + break; + } + case 56464: + { + var dst = this.S("q"); + var globalName = this.E(); + if (!(globalName in this.Y)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.Y[globalName]; + break; + } + case 34526: + { + var dst = this.S("y"); + regs[base + dst] = regs[fp + 3].C[this.S({})].ah(null, undefined); + break; + } + case 2151: + { + var dst = this.S(93); + regs[base + dst] = regs[fp + 2]; + break; + } + case 21164: + { + var dst = this.S({}); + regs[base + dst] = regs[base + this.S({})]; + break; + } + case 4576: + { + // globals[globalName] = regs[src] + this.Y[this.E()] = regs[base + this.S(undefined)]; + break; + } + case 43086: + { + var uvIdx = this.S({}); + regs[fp + 3].C[uvIdx].O(null, regs[base + this.S(67)], "y"); + break; + } + case 63110: + { + // dst = regs[obj][regs[key]] + var dst = this.S(null); + var obj = regs[base + this.S({})]; + var key = regs[base + this.S(75)]; + regs[base + dst] = obj[key]; + break; + } + case 45975: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.S({})]; + var key = regs[base + this.S(23)]; + var val = regs[base + this.S("k")]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 59697: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.S(null); + var obj = regs[base + this.S(43)]; + var key = regs[base + this.S("z")]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 37279: + { + var dst = this.S(undefined); + var a = regs[base + this.S({})]; + regs[base + dst] = a + regs[base + this.S([])]; + break; + } + case 363: + { + var dst = this.S(undefined); + var a = regs[base + this.S([])]; + regs[base + dst] = a - regs[base + this.S({})]; + break; + } + case 64040: + { + var dst = this.S([]); + var a = regs[base + this.S(22)]; + regs[base + dst] = a * regs[base + this.S(null)]; + break; + } + case 64597: + { + var dst = this.S(undefined); + var a = regs[base + this.S([])]; + regs[base + dst] = a / regs[base + this.S([])]; + break; + } + case 64807: + { + var dst = this.S(undefined); + var a = regs[base + this.S({})]; + regs[base + dst] = a % regs[base + this.S("k")]; + break; + } + case 30314: + { + // Math.pow instead of `**` + var dst = this.S("q"); + var a = regs[base + this.S({})]; + regs[base + dst] = Math.pow(a, regs[base + this.S(null)]); + break; + } + case 24955: + { + var dst = this.S(45); + var a = regs[base + this.S(null)]; + regs[base + dst] = a & regs[base + this.S("k")]; + break; + } + case 57871: + { + var dst = this.S(null); + var a = regs[base + this.S("q")]; + regs[base + dst] = a | regs[base + this.S([])]; + break; + } + case 5417: + { + var dst = this.S(undefined); + var a = regs[base + this.S("k")]; + regs[base + dst] = a ^ regs[base + this.S([])]; + break; + } + case 9866: + { + var dst = this.S([]); + var a = regs[base + this.S("x")]; + regs[base + dst] = a << regs[base + this.S(null)]; + break; + } + case 12140: + { + var dst = this.S(30); + var a = regs[base + this.S(null)]; + regs[base + dst] = a >> regs[base + this.S([])]; + break; + } + case 46998: + { + var dst = this.S(73); + var a = regs[base + this.S(31)]; + regs[base + dst] = a >>> regs[base + this.S(undefined)]; + break; + } + + // Comparison (dst, src1, src2) + case 18923: + { + var dst = this.S("q"); + var a = regs[base + this.S("z")]; + regs[base + dst] = a < regs[base + this.S(undefined)]; + break; + } + case 23357: + { + var dst = this.S([]); + var a = regs[base + this.S(undefined)]; + regs[base + dst] = a > regs[base + this.S(null)]; + break; + } + case 64658: + { + var dst = this.S(null); + var a = regs[base + this.S(27)]; + regs[base + dst] = a <= regs[base + this.S(undefined)]; + break; + } + case 29044: + { + var dst = this.S(48); + var a = regs[base + this.S("q")]; + regs[base + dst] = a >= regs[base + this.S(86)]; + break; + } + case 25819: + { + var dst = this.S({}); + var a = regs[base + this.S({})]; + regs[base + dst] = a === regs[base + this.S(null)]; + break; + } + case 39164: + { + var dst = this.S(undefined); + var a = regs[base + this.S({})]; + regs[base + dst] = a !== regs[base + this.S("z")]; + break; + } + case 8215: + { + var dst = this.S([]); + var a = regs[base + this.S(undefined)]; + regs[base + dst] = a == regs[base + this.S([])]; + break; + } + case 31978: + { + var dst = this.S({}); + var a = regs[base + this.S(null)]; + regs[base + dst] = a != regs[base + this.S([])]; + break; + } + case 21914: + { + var dst = this.S(undefined); + var a = regs[base + this.S(null)]; + regs[base + dst] = a in regs[base + this.S([])]; + break; + } + case 6605: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.S(94); + var obj = regs[base + this.S(undefined)]; + regs[base + dst] = obj instanceof regs[base + this.S([])]; + break; + } + + // Unary (dst, src) + case 57697: + { + var dst = this.S(undefined); + regs[base + dst] = -regs[base + this.S([])]; + break; + } + case 23644: + { + var dst = this.S(undefined); + regs[base + dst] = +regs[base + this.S(undefined)]; + break; + } + case 43518: + { + var dst = this.S(null); + regs[base + dst] = !regs[base + this.S([])]; + break; + } + case 17920: + { + var dst = this.S([]); + regs[base + dst] = ~regs[base + this.S({})]; + break; + } + case 39066: + { + var dst = this.S(null); + regs[base + dst] = typeof regs[base + this.S(null)]; + break; + } + case 2548: + { + var dst = this.S(undefined); + this.S(3); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 36661: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.S(undefined); + var name = this.E(); + var val = Object.prototype.hasOwnProperty.call(this.Y, name) ? this.Y[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 39919: + regs[fp + 1] = this.S(undefined); + break; + case 37081: + { + var src = this.S(null); + var target = this.S({}); + if (!regs[base + src]) regs[fp + 1] = target; + break; + } + case 5524: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.S("k"); + var target = this.S(null); + if (regs[base + src]) regs[fp + 1] = target; + break; + } + + // Calls + case 4545: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.S(null); + var callee = regs[base + this.S(null)]; + var argc = this.S({}); + var args; + if (argc === 3847166315) { + args = regs[base + this.S(40)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.S([])]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.ag({}, null, [], this.Y, dst << 1, args, closure); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 15913: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.S({}); + var receiver = regs[base + this.S(52)]; + var callee = regs[base + this.S(undefined)]; + var argc = this.S(17); + var args; + if (argc === 3847166315) { + args = regs[base + this.S([])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.S(59)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.ag(undefined, null, {}, receiver, dst << 1, args, closure); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 52616: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.S(undefined); + var callee = regs[base + this.S(32)]; + var argc = this.S(null); + var args; + if (argc === 3847166315) { + args = regs[base + this.S({})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.S(undefined)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.ag(undefined, "y", {}, Object.create(closure.prototype || null), dst << 1 | 1, args, closure); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 62456: + { + var retVal = regs[base + this.S("k")]; + this.b({}, 41, undefined, "k", fp); // must happen before frame is abandoned + + var caller = regs[fp + 10]; + var retDst = regs[fp + 8]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 2]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 4]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.R = fp; + this.P = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 7] + (retDst >> 1) : 0] = retVal; + break; + } + case 19563: + throw regs[base + this.S(undefined)]; + + // Closures + case 18527: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.S("k"); + var startPc = this.S("k"); + var paramCount = this.S(null); + var regCount = this.S("z"); + var uvCount = this.S([]); + var hasRest = this.S({}); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.S("x"); + var uvIndex = this.S(75); + uvDescs[i] = { + V: isLocalRaw, + m: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + U: paramCount, + q: regCount, + ab: startPc, + z: hasRest + }; + var closure = new Closure("z", [], fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.V) { + closure.C.push(this.aa({}, fp, 23, [], uvd.m, [])); + } else { + closure.C.push(regs[fp + 3].C[uvd.m]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(21, self.v, self.X, [], {}, self.Y).t(c, Array.prototype.slice.call(arguments), null, this == null ? self.Y : this, "z", 93); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 28412: + { + // dst, count, [elemReg...] + var dst = this.S(null); + var count = this.S({}); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.S({})]; + regs[base + dst] = elems; + break; + } + case 53382: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.S([]); + var pairCount = this.S(35); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.S({})]; + var val = regs[base + this.S(40)]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 1716: + { + // obj, key, fn + var obj = regs[base + this.S(null)]; + var key = regs[base + this.S("x")]; + var getterFn = regs[base + this.S(null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 28189: + { + // obj, key, fn + var obj = regs[base + this.S(undefined)]; + var key = regs[base + this.S(null)]; + var setterFn = regs[base + this.S(undefined)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 52804: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.S([]); + var obj = regs[base + this.S([])]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + D: keys, + c: 0 + }; + break; + } + case 8387: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.S(undefined); + var iter = regs[base + this.S([])]; + var exitTarget = this.S("y"); + if (iter.c >= iter.D.length) { + regs[fp + 1] = exitTarget; + } else { + regs[base + dst] = iter.D[iter.c++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 50691: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 11]; + if (!hs) regs[fp + 11] = hs = []; + hs.push({ + I: this.S("x"), + L: this.S("y") + }); + break; + } + case 40557: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 11].pop(); + break; + } + case 57952: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 11]; + if (!hs) regs[fp + 11] = hs = []; + hs.push({ + y: this.S(null), + ae: this.S(69), + B: this.S(undefined), + ad: this.S(undefined) + }); + break; + } + + // Self-modifying bytecode + case 32453: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.S(undefined); + var sliceStart = this.S("q"); + var sliceEnd = this.S(null); + var pk = this.S([]) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.v[destPc + (pi - sliceStart)] = (this.v[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 13119: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 1] = regs[base + this.S([])]; + break; + } + case 54257: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 1] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.P; + while (searchFrame) { + var handlers = regs[searchFrame + 11]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.b(null, [], [], undefined, searchFrame); + this.R = searchFrame; + searchFrame = regs[searchFrame + 10]; + this.P = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 11].pop(); + var hBase = regs[handledFrame + 7]; + if (h.L !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.L] = err; + regs[handledFrame + 1] = h.I; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.ae] = h.ad; + regs[hBase + h.B] = err; + regs[handledFrame + 1] = h.y; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.R = handledFrame + regs[handledFrame + 4]; + this.P = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.P) return this.f[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(82, decodeBytecode(BYTECODE), CONSTANTS, 31, [], globals); +vm.t(new Closure(undefined, 90, { + U: 0, + q: 9, + ab: 0 +}), null // no arguments object / parameter setup for the root frame +, 45, undefined, [], 33); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-182.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-182.js new file mode 100644 index 00000000..155c9d4f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-182.js @@ -0,0 +1,952 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) + +// Closure & Frame +function Closure(fn, fake_6) { + this.B = fn; + this.E = []; + this.prototype = {}; // <- default prototype object for `new` + this.O = fake_6; +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +var CONSTANTS = [/* 0 */2, /* 1 */5, /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */"concat", /* 5 */undefined]; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function VM(bytecode, fake_7, globals, constants) { + this.J = bytecode; + this.A = constants; + this.U = fake_7; + this.d = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.W = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.T = []; + this.u = 8; + this.o = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +function Upvalue(absSlot, fake_1, regs) { + this.e = fake_1; + this.T = regs; // shared reference to VM._regs flat array + this.f = absSlot; // absolute index; stable as long as frame is alive + this.p = false; + this.j = undefined; +} +var BYTECODE = [49294, 1, 10642, 4, 68, 2, 5, 0, 0, 12815, 2, 4, 63273, 4, 0, 0, 63273, 5, 1, 0, 49940, 6, 2, 4, 5, 12815, 3, 6, 44053, 4, 2, 0, 63273, 5, 3, 0, 49940, 6, 0, 63273, 7, 4, 0, 55279, 8, 6, 7, 27168, 7, 6, 8, 1, 3, 39948, 6, 2, 1404279311, 7, 5872, 4, 5, 6, 63273, 4, 5, 0, 13431, 4, 49294, 3, 38978, 4, 0, 1, 13431, 4, 63273, 4, 5, 0, 13431, 4]; +Upvalue.prototype.i = function (fake_2) { + this.Y = fake_2; + return this.p ? this.j : this.T[this.f]; +}; +Upvalue.prototype.s = function (fake_4, fake_5) { + this.j = this.T[this.f]; + this.r = fake_5; + this.p = true; + this.a = fake_4; +}; +VM.prototype.H = function (fp, fake_17) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.W; + this.N = fake_17; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this.T; + var hi = fp + regs[fp + 4]; + for (var i = regs[fp + 0]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.s(65, undefined); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +Upvalue.prototype.y = function (fake_3, v) { + if (this.p) this.j = v;else this.T[this.f] = v; + this.k = fake_3; +}; +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.c = function (fake_16, keyIn, idxIn) { + var idx = idxIn ?? this.L(27, null, null); + var key = keyIn ?? this.L("y", {}, undefined); + this.g = fake_16; + var v = this.A[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype.V = function (fake_15, fake_13, fp, fake_14, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.T[fp + 0] + slot; + var uvs = this.W || (this.W = []); + this.w = fake_13; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, undefined, this.T)); + this.t = [fake_15, fake_14]; +}; +VM.prototype.L = function (fake_10, fake_9, fake_8) { + this.n = fake_8; + this.m = fake_9 && fake_10; + return this.J[this.T[this.o + 7]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.I = function (retDst, args, fake_11, fake_12, closure, thisVal) { + var fn = closure.B; + var regs = this.T; + var fp = this.u; + var size = 10 + fn.C; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 10; + regs[fp + 7] = fn.F; + this.G = [fake_11, fake_12]; + regs[fp + 2] = this.o; + regs[fp + 1] = retDst; + regs[fp + 3] = thisVal; + regs[fp + 6] = closure; + regs[fp + 4] = size; + regs[fp + 0] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof 8 === "number") regs[fp + 8] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.X; + if (typeof void 0 === "number") regs[fp + void 0] = args; + if (typeof void 0 === "number") regs[fp + void 0] = fn.F; + if (typeof void 0 === "number") regs[fp + void 0] = 0; + this.u = end; + if (args) { + if (fn.z) { + var restSlot = fn.X - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.C; a++) regs[base + a] = args[a]; + } + if (fn.X < fn.C) regs[base + fn.X] = args; + } + this.o = fp; +}; +VM.prototype.q = function (args, fake_18, fake_19, closure, thisVal, fake_20) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this.h = [fake_19, fake_20]; + this.I(0, args, 42, {}, closure, thisVal); + while (true) { + var fp = this.o; + var regs = this.T; + var bc = this.J; + var pc = regs[fp + 7]; + if (pc >= bc.length) break; + regs[fp + 7] = pc + 1; + var op = this.J[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof void 0 === "number") regs[fp + void 0] = (regs[fp + void 0] + 1) % bc.length; + if (typeof void 0 === "number") regs[fp + void 0]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.J.length; i++) this.J[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.u; i2++) this.T[i2] = undefined; + op = 7297; + regs[fp + 7] = this.J.length; // jump past end to halt + } + } + try { + var base = regs[fp + 0]; + + /* @SWITCH */ + switch (op) { + case 63273: + { + var dst = this.L("x", "k", {}); + regs[base + dst] = this.c(); + break; + } + case 2068: + { + var dst = this.L({}, undefined, 52); + regs[base + dst] = this.L(null, undefined, "k"); + break; + } + case 44053: + { + var dst = this.L({}, undefined, undefined); + var globalName = this.c(); + if (!(globalName in this.d)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.d[globalName]; + break; + } + case 41765: + { + var dst = this.L(null, null, undefined); + regs[base + dst] = regs[fp + 6].E[this.L(81, null, "x")].i([]); + break; + } + case 49294: + { + var dst = this.L({}, "z", null); + regs[base + dst] = regs[fp + 3]; + break; + } + case 12815: + { + var dst = this.L(null, "k", {}); + regs[base + dst] = regs[base + this.L(null, "k", "z")]; + break; + } + case 28556: + { + // globals[globalName] = regs[src] + this.d[this.c()] = regs[base + this.L({}, {}, null)]; + break; + } + case 55066: + { + var uvIdx = this.L(77, null, null); + regs[fp + 6].E[uvIdx].y(44, regs[base + this.L(undefined, 31, 81)]); + break; + } + case 55279: + { + // dst = regs[obj][regs[key]] + var dst = this.L("q", [], 51); + var obj = regs[base + this.L(undefined, {}, "y")]; + var key = regs[base + this.L(10, 37, [])]; + regs[base + dst] = obj[key]; + break; + } + case 5872: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.L({}, undefined, null)]; + var key = regs[base + this.L(undefined, "z", [])]; + var val = regs[base + this.L(75, 6, {})]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 63910: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.L(null, null, "q"); + var obj = regs[base + this.L(null, null, null)]; + var key = regs[base + this.L(null, undefined, {})]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 38978: + { + var dst = this.L(85, "k", undefined); + var a = regs[base + this.L(null, 85, null)]; + regs[base + dst] = a + regs[base + this.L([], {}, null)]; + break; + } + case 1702: + { + var dst = this.L("k", undefined, []); + var a = regs[base + this.L(null, {}, {})]; + regs[base + dst] = a - regs[base + this.L("y", undefined, 28)]; + break; + } + case 59734: + { + var dst = this.L("z", "y", undefined); + var a = regs[base + this.L(27, undefined, undefined)]; + regs[base + dst] = a * regs[base + this.L(null, [], null)]; + break; + } + case 65274: + { + var dst = this.L("k", 61, null); + var a = regs[base + this.L(84, null, {})]; + regs[base + dst] = a / regs[base + this.L(undefined, [], {})]; + break; + } + case 11271: + { + var dst = this.L(undefined, 27, {}); + var a = regs[base + this.L([], "q", null)]; + regs[base + dst] = a % regs[base + this.L("x", undefined, undefined)]; + break; + } + case 18230: + { + // Math.pow instead of `**` + var dst = this.L(80, 33, 73); + var a = regs[base + this.L(45, [], 41)]; + regs[base + dst] = Math.pow(a, regs[base + this.L([], undefined, [])]); + break; + } + case 21828: + { + var dst = this.L(null, 49, "z"); + var a = regs[base + this.L(undefined, [], {})]; + regs[base + dst] = a & regs[base + this.L("x", 35, "x")]; + break; + } + case 32609: + { + var dst = this.L(null, 78, null); + var a = regs[base + this.L(null, 35, undefined)]; + regs[base + dst] = a | regs[base + this.L("q", {}, {})]; + break; + } + case 15560: + { + var dst = this.L(null, {}, []); + var a = regs[base + this.L({}, {}, null)]; + regs[base + dst] = a ^ regs[base + this.L(undefined, undefined, "z")]; + break; + } + case 51471: + { + var dst = this.L(undefined, {}, undefined); + var a = regs[base + this.L(undefined, "q", {})]; + regs[base + dst] = a << regs[base + this.L({}, undefined, "z")]; + break; + } + case 50846: + { + var dst = this.L({}, undefined, {}); + var a = regs[base + this.L({}, undefined, [])]; + regs[base + dst] = a >> regs[base + this.L("k", "q", 47)]; + break; + } + case 44691: + { + var dst = this.L(9, {}, null); + var a = regs[base + this.L(null, undefined, {})]; + regs[base + dst] = a >>> regs[base + this.L(null, undefined, null)]; + break; + } + + // Comparison (dst, src1, src2) + case 11714: + { + var dst = this.L(62, undefined, 0); + var a = regs[base + this.L(9, 38, undefined)]; + regs[base + dst] = a < regs[base + this.L({}, {}, null)]; + break; + } + case 28433: + { + var dst = this.L(91, null, "k"); + var a = regs[base + this.L(61, [], "k")]; + regs[base + dst] = a > regs[base + this.L({}, [], undefined)]; + break; + } + case 4517: + { + var dst = this.L(null, undefined, undefined); + var a = regs[base + this.L([], undefined, undefined)]; + regs[base + dst] = a <= regs[base + this.L(undefined, [], 65)]; + break; + } + case 59488: + { + var dst = this.L("x", "q", null); + var a = regs[base + this.L(null, undefined, undefined)]; + regs[base + dst] = a >= regs[base + this.L({}, null, 72)]; + break; + } + case 64647: + { + var dst = this.L(undefined, 4, 52); + var a = regs[base + this.L({}, null, undefined)]; + regs[base + dst] = a === regs[base + this.L({}, undefined, {})]; + break; + } + case 13217: + { + var dst = this.L("q", "z", "q"); + var a = regs[base + this.L("k", null, "x")]; + regs[base + dst] = a !== regs[base + this.L({}, undefined, 12)]; + break; + } + case 45154: + { + var dst = this.L({}, [], {}); + var a = regs[base + this.L({}, 69, "k")]; + regs[base + dst] = a == regs[base + this.L({}, "q", "q")]; + break; + } + case 44273: + { + var dst = this.L(52, 27, {}); + var a = regs[base + this.L(null, undefined, null)]; + regs[base + dst] = a != regs[base + this.L([], null, null)]; + break; + } + case 23801: + { + var dst = this.L({}, [], 25); + var a = regs[base + this.L([], [], 96)]; + regs[base + dst] = a in regs[base + this.L("x", undefined, [])]; + break; + } + case 1968: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.L(null, "q", 63); + var obj = regs[base + this.L({}, undefined, "k")]; + regs[base + dst] = obj instanceof regs[base + this.L([], 72, null)]; + break; + } + + // Unary (dst, src) + case 23038: + { + var dst = this.L([], undefined, {}); + regs[base + dst] = -regs[base + this.L(undefined, [], "x")]; + break; + } + case 2315: + { + var dst = this.L(undefined, null, "x"); + regs[base + dst] = +regs[base + this.L({}, undefined, null)]; + break; + } + case 54482: + { + var dst = this.L(undefined, 22, undefined); + regs[base + dst] = !regs[base + this.L("x", {}, null)]; + break; + } + case 19322: + { + var dst = this.L(12, undefined, null); + regs[base + dst] = ~regs[base + this.L(53, {}, undefined)]; + break; + } + case 20202: + { + var dst = this.L(85, 49, null); + regs[base + dst] = typeof regs[base + this.L({}, "k", "k")]; + break; + } + case 49339: + { + var dst = this.L([], {}, undefined); + this.L("q", {}, {}); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 11916: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.L(37, 30, "z"); + var name = this.c(); + var val = Object.prototype.hasOwnProperty.call(this.d, name) ? this.d[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 7297: + regs[fp + 7] = this.L({}, "x", "q"); + break; + case 64871: + { + var src = this.L(undefined, [], "x"); + var target = this.L(undefined, undefined, []); + if (!regs[base + src]) regs[fp + 7] = target; + break; + } + case 17601: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.L(undefined, {}, "x"); + var target = this.L("k", undefined, 0); + if (regs[base + src]) regs[fp + 7] = target; + break; + } + + // Calls + case 39948: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.L("q", null, {}); + var callee = regs[base + this.L(undefined, "k", {})]; + var argc = this.L([], [], null); + var args; + if (argc === 1404279311) { + args = regs[base + this.L({}, undefined, "y")]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.L([], undefined, 3)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.I(dst << 1, args, {}, null, closure, this.d); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 27168: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.L([], "q", "z"); + var receiver = regs[base + this.L(undefined, 30, [])]; + var callee = regs[base + this.L(33, null, {})]; + var argc = this.L({}, [], {}); + var args; + if (argc === 1404279311) { + args = regs[base + this.L("q", {}, undefined)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.L(null, 57, "x")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.I(dst << 1, args, null, [], closure, receiver); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 58650: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.L([], undefined, null); + var callee = regs[base + this.L("z", null, {})]; + var argc = this.L({}, null, []); + var args; + if (argc === 1404279311) { + args = regs[base + this.L("k", undefined, {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.L(25, "x", "y")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.I(dst << 1 | 1, args, [], {}, closure, Object.create(closure.prototype || null)); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 13431: + { + var retVal = regs[base + this.L(undefined, {}, [])]; + this.H(fp, 53); // must happen before frame is abandoned + + var caller = regs[fp + 2]; + var retDst = regs[fp + 1]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 4]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.u = fp; + this.o = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 0] + (retDst >> 1) : 0] = retVal; + break; + } + case 56258: + throw regs[base + this.L(undefined, null, [])]; + + // Closures + case 10642: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.L(undefined, "y", "k"); + var startPc = this.L(11, null, undefined); + var paramCount = this.L("x", undefined, "x"); + var regCount = this.L({}, undefined, []); + var uvCount = this.L("q", null, {}); + var hasRest = this.L([], [], {}); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.L({}, "q", {}); + var uvIndex = this.L([], 91, undefined); + uvDescs[i] = { + l: isLocalRaw, + D: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + X: paramCount, + C: regCount, + F: startPc, + z: hasRest + }; + var closure = new Closure(fn, []); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.l) { + closure.E.push(this.V([], undefined, fp, [], uvd.D)); + } else { + closure.E.push(regs[fp + 6].E[uvd.D]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.J, [], self.d, self.A).q(Array.prototype.slice.call(arguments), [], "k", c, this == null ? self.d : this, 82); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 49940: + { + // dst, count, [elemReg...] + var dst = this.L(24, undefined, {}); + var count = this.L({}, null, "q"); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.L("k", "q", null)]; + regs[base + dst] = elems; + break; + } + case 64668: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.L(null, null, 25); + var pairCount = this.L({}, {}, 2); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.L([], undefined, [])]; + var val = regs[base + this.L([], null, [])]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 24330: + { + // obj, key, fn + var obj = regs[base + this.L({}, [], null)]; + var key = regs[base + this.L([], null, undefined)]; + var getterFn = regs[base + this.L("y", "x", undefined)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 64651: + { + // obj, key, fn + var obj = regs[base + this.L(14, {}, [])]; + var key = regs[base + this.L(undefined, 75, [])]; + var setterFn = regs[base + this.L(46, 47, null)]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 65017: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.L(null, 71, []); + var obj = regs[base + this.L(null, {}, [])]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + P: keys, + S: 0 + }; + break; + } + case 10020: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.L({}, "q", {}); + var iter = regs[base + this.L("z", 63, 5)]; + var exitTarget = this.L({}, {}, "k"); + if (iter.S >= iter.P.length) { + regs[fp + 7] = exitTarget; + } else { + regs[base + dst] = iter.P[iter.S++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 19519: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 9]; + if (!hs) regs[fp + 9] = hs = []; + hs.push({ + R: this.L(undefined, {}, undefined), + b: this.L([], 72, 56) + }); + break; + } + case 13991: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 9].pop(); + break; + } + case 40589: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 9]; + if (!hs) regs[fp + 9] = hs = []; + hs.push({ + x: this.L(undefined, 66, 29), + M: this.L({}, [], 64), + K: this.L("q", 39, {}), + v: this.L(null, [], null) + }); + break; + } + + // Self-modifying bytecode + case 1001: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.L([], [], 2); + var sliceStart = this.L("z", {}, {}); + var sliceEnd = this.L(59, [], {}); + var pk = this.L([], null, undefined) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.J[destPc + (pi - sliceStart)] = (this.J[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 24210: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 7] = regs[base + this.L([], 90, "x")]; + break; + } + case 27223: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 7] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.o; + while (searchFrame) { + var handlers = regs[searchFrame + 9]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.H(searchFrame, 59); + this.u = searchFrame; + searchFrame = regs[searchFrame + 2]; + this.o = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 9].pop(); + var hBase = regs[handledFrame + 0]; + if (h.b !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.b] = err; + regs[handledFrame + 7] = h.R; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.M] = h.v; + regs[hBase + h.K] = err; + regs[handledFrame + 7] = h.x; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.u = handledFrame + regs[handledFrame + 4]; + this.o = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.o) return this.T[0]; + } + this.Q = fake_18; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), null, globals, CONSTANTS); +vm.q(null // no arguments object / parameter setup for the root frame +, {}, {}, new Closure({ + X: 0, + C: 9, + F: 0 +}, undefined), undefined, null); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-183.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-183.js new file mode 100644 index 00000000..231d35ef --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-183.js @@ -0,0 +1,958 @@ +function decodeBytecode(s) { + if (!false) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CONSTANTS = [/* 0 */2, /* 1 */5, /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */"concat", /* 5 */undefined]; +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(absSlot, fake_4, fake_2, fake_3, fake_1, regs) { + this.e = fake_1; + this.p = regs; // shared reference to VM._regs flat array + this.n = fake_4; + this.r = absSlot; // absolute index; stable as long as frame is alive + this.g = false; + this.ab = fake_3; + this.y = fake_2; + this.ag = undefined; +} +// Closure & Frame +function Closure(fn, fake_11) { + this.z = fn; + this.ad = []; + this.prototype = {}; // <- default prototype object for `new` + this.h = fake_11; +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, fake_12, globals) { + this.b = bytecode; + this.j = constants; + this.f = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this.k = fake_12; + this.U = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this.p = []; + this.S = 7; + this.m = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +var BYTECODE = [63827, 1, 63522, 4, 68, 2, 5, 0, 0, 25408, 2, 4, 1783, 4, 0, 0, 1783, 5, 1, 0, 57913, 6, 2, 4, 5, 25408, 3, 6, 38527, 4, 2, 0, 1783, 5, 3, 0, 57913, 6, 0, 1783, 7, 4, 0, 1227, 8, 6, 7, 6030, 7, 6, 8, 1, 3, 45847, 6, 2, 2617719534, 7, 42057, 4, 5, 6, 1783, 4, 5, 0, 4387, 4, 63827, 3, 60230, 4, 0, 1, 4387, 4, 1783, 4, 5, 0, 4387, 4]; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +var CLOSURE_MAP = new WeakMap(); +VM.prototype.a = function (fake_16, fake_13, fake_14, fake_15) { + return this.b[this.p[this.m + 4]++]; + this.c = [fake_13, fake_16]; + this.J = [fake_15, fake_14]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype.Q = function (retDst, fake_19, thisVal, fake_17, args, closure, fake_18) { + var fn = closure.z; + var regs = this.p; + var fp = this.S; + var size = 16 + fn.l; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + 16; + regs[fp + 4] = fn.H; + regs[fp + 13] = this.m; + regs[fp + 2] = retDst; + regs[fp + 3] = thisVal; + this.af = fake_17 || fake_18; + regs[fp + 8] = closure; + regs[fp + 15] = size; + regs[fp + 10] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + this.L = fake_19; + if (typeof 12 === "number") regs[fp + 12] = end; + if (typeof void 0 === "number") regs[fp + void 0] = fn.V; + if (typeof 7 === "number") regs[fp + 7] = args; + if (typeof 1 === "number") regs[fp + 1] = fn.H; + if (typeof 9 === "number") regs[fp + 9] = 0; + this.S = end; + if (args) { + if (fn.ae) { + var restSlot = fn.V - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.l; a++) regs[base + a] = args[a]; + } + if (fn.V < fn.l) regs[base + fn.V] = args; + } + this.m = fp; +}; +Upvalue.prototype.t = function (fake_7, fake_9, fake_10, fake_8) { + this.F = fake_10; + this.ag = this.p[this.r]; + this.g = true; + this.R = fake_7; + this.ac = fake_8; + this.D = fake_9; +}; +VM.prototype.P = function (fake_25, fake_26, fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this.U; + if (!uvs) return; // nothing in this program ever captured a local + this.T = fake_25 && fake_26; + var regs = this.p; + var hi = fp + regs[fp + 15]; + for (var i = regs[fp + 10]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv.t("x", undefined, undefined, 53); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype.G = function (fake_23, keyIn, fake_24, idxIn) { + var idx = idxIn ?? this.a(62, 27, null, "k"); + var key = keyIn ?? this.a({}, {}, "y", undefined); + var v = this.j[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + this.M = fake_24; + var out = ""; + this.E = fake_23; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +Upvalue.prototype.i = function (fake_5) { + return this.g ? this.ag : this.p[this.r]; + this.v = fake_5; +}; +VM.prototype.W = function (slot, fake_21, fake_20, fake_22, fp) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this.p[fp + 10] + slot; + this.u = fake_21; + var uvs = this.U || (this.U = []); + this.Z = fake_22 && fake_20; + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(absSlot, 24, undefined, null, [], this.p)); +}; +Upvalue.prototype.B = function (v, fake_6) { + if (this.g) this.ag = v;else this.p[this.r] = v; + this.d = fake_6; +}; +VM.prototype.I = function (closure, fake_28, thisVal, fake_27, fake_29, fake_30, args) { + var now = () => { + return performance.now(); + }; + this.X = [fake_27, fake_29]; + this.K = fake_28; + var lastTime = now(); + this.Q(0, undefined, thisVal, [], args, closure, {}); + while (true) { + var fp = this.m; + var regs = this.p; + var bc = this.b; + var pc = regs[fp + 4]; + if (pc >= bc.length) break; + regs[fp + 4] = pc + 1; + var op = this.b[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof 1 === "number") regs[fp + 1] = (regs[fp + 1] + 1) % bc.length; + if (typeof 9 === "number") regs[fp + 9]++; + if (typeof void 0 === "number") regs[fp + void 0] = op; + if (typeof void 0 === "number") regs[fp + void 0] = regs[fp + void 0] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (false) { + var currentTime = now(); + var isTamper = currentTime - lastTime > false; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.b.length; i++) this.b[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this.S; i2++) this.p[i2] = undefined; + op = 19772; + regs[fp + 4] = this.b.length; // jump past end to halt + } + } + try { + var base = regs[fp + 10]; + + /* @SWITCH */ + switch (op) { + case 1783: + { + var dst = this.a([], [], [], null); + regs[base + dst] = this.G(); + break; + } + case 36363: + { + var dst = this.a(undefined, "k", [], null); + regs[base + dst] = this.a(undefined, {}, null, {}); + break; + } + case 38527: + { + var dst = this.a([], "k", "z", {}); + var globalName = this.G(); + if (!(globalName in this.f)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.f[globalName]; + break; + } + case 41838: + { + var dst = this.a(undefined, [], null, null); + regs[base + dst] = regs[fp + 8].ad[this.a("y", "k", null, 93)].i(42); + break; + } + case 63827: + { + var dst = this.a(34, [], {}, []); + regs[base + dst] = regs[fp + 3]; + break; + } + case 25408: + { + var dst = this.a(undefined, {}, null, 4); + regs[base + dst] = regs[base + this.a([], null, {}, {})]; + break; + } + case 51321: + { + // globals[globalName] = regs[src] + this.f[this.G()] = regs[base + this.a({}, "k", {}, "y")]; + break; + } + case 53456: + { + var uvIdx = this.a("y", undefined, undefined, null); + regs[fp + 8].ad[uvIdx].B(regs[base + this.a({}, "q", null, [])], null); + break; + } + case 1227: + { + // dst = regs[obj][regs[key]] + var dst = this.a(null, [], undefined, null); + var obj = regs[base + this.a({}, "y", {}, "k")]; + var key = regs[base + this.a({}, [], "z", null)]; + regs[base + dst] = obj[key]; + break; + } + case 42057: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this.a(83, {}, {}, null)]; + var key = regs[base + this.a([], [], [], 70)]; + var val = regs[base + this.a(undefined, undefined, undefined, null)]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 65260: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this.a(undefined, "q", null, undefined); + var obj = regs[base + this.a(76, undefined, 41, 95)]; + var key = regs[base + this.a([], undefined, null, [])]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case 60230: + { + var dst = this.a(undefined, {}, {}, []); + var a = regs[base + this.a({}, 19, undefined, "x")]; + regs[base + dst] = a + regs[base + this.a([], "k", undefined, {})]; + break; + } + case 3760: + { + var dst = this.a({}, 28, [], null); + var a = regs[base + this.a({}, {}, undefined, {})]; + regs[base + dst] = a - regs[base + this.a(undefined, {}, [], [])]; + break; + } + case 56749: + { + var dst = this.a({}, 37, 45, []); + var a = regs[base + this.a(null, undefined, "z", 43)]; + regs[base + dst] = a * regs[base + this.a(undefined, undefined, 5, [])]; + break; + } + case 35397: + { + var dst = this.a([], "y", 34, "z"); + var a = regs[base + this.a("k", 54, "z", undefined)]; + regs[base + dst] = a / regs[base + this.a(65, 43, undefined, "x")]; + break; + } + case 38326: + { + var dst = this.a(null, {}, {}, 56); + var a = regs[base + this.a(undefined, [], 89, undefined)]; + regs[base + dst] = a % regs[base + this.a(null, null, 91, {})]; + break; + } + case 52601: + { + // Math.pow instead of `**` + var dst = this.a("q", null, 3, 16); + var a = regs[base + this.a("q", "q", [], {})]; + regs[base + dst] = Math.pow(a, regs[base + this.a(43, {}, undefined, 79)]); + break; + } + case 7156: + { + var dst = this.a(null, "x", 43, {}); + var a = regs[base + this.a(null, 42, {}, null)]; + regs[base + dst] = a & regs[base + this.a("q", 14, "y", undefined)]; + break; + } + case 33721: + { + var dst = this.a(null, "k", null, []); + var a = regs[base + this.a(20, null, undefined, 75)]; + regs[base + dst] = a | regs[base + this.a(undefined, null, null, "y")]; + break; + } + case 40723: + { + var dst = this.a(undefined, null, null, []); + var a = regs[base + this.a("z", 84, null, undefined)]; + regs[base + dst] = a ^ regs[base + this.a("z", [], "y", null)]; + break; + } + case 46661: + { + var dst = this.a(null, "x", "y", undefined); + var a = regs[base + this.a({}, 39, {}, undefined)]; + regs[base + dst] = a << regs[base + this.a(undefined, undefined, {}, [])]; + break; + } + case 9322: + { + var dst = this.a(null, {}, undefined, []); + var a = regs[base + this.a({}, [], 20, {})]; + regs[base + dst] = a >> regs[base + this.a([], [], 2, null)]; + break; + } + case 64414: + { + var dst = this.a(62, "q", undefined, []); + var a = regs[base + this.a(null, 78, 36, {})]; + regs[base + dst] = a >>> regs[base + this.a(undefined, null, null, [])]; + break; + } + + // Comparison (dst, src1, src2) + case 41605: + { + var dst = this.a("x", null, 72, "q"); + var a = regs[base + this.a(undefined, undefined, null, "q")]; + regs[base + dst] = a < regs[base + this.a(undefined, undefined, "x", 64)]; + break; + } + case 5756: + { + var dst = this.a(null, null, {}, null); + var a = regs[base + this.a(undefined, "z", null, {})]; + regs[base + dst] = a > regs[base + this.a(undefined, [], undefined, {})]; + break; + } + case 40098: + { + var dst = this.a(null, [], [], 97); + var a = regs[base + this.a(undefined, undefined, "x", 56)]; + regs[base + dst] = a <= regs[base + this.a(71, undefined, "z", "y")]; + break; + } + case 171: + { + var dst = this.a(null, [], [], []); + var a = regs[base + this.a("z", undefined, "x", undefined)]; + regs[base + dst] = a >= regs[base + this.a("x", {}, {}, null)]; + break; + } + case 11047: + { + var dst = this.a(null, 58, "k", null); + var a = regs[base + this.a({}, undefined, null, {})]; + regs[base + dst] = a === regs[base + this.a([], {}, [], null)]; + break; + } + case 38434: + { + var dst = this.a(undefined, {}, 58, null); + var a = regs[base + this.a(null, undefined, {}, undefined)]; + regs[base + dst] = a !== regs[base + this.a(null, {}, "z", null)]; + break; + } + case 23794: + { + var dst = this.a(undefined, undefined, {}, null); + var a = regs[base + this.a(4, 80, "y", "k")]; + regs[base + dst] = a == regs[base + this.a("y", [], {}, [])]; + break; + } + case 52535: + { + var dst = this.a(null, [], null, null); + var a = regs[base + this.a(78, {}, [], undefined)]; + regs[base + dst] = a != regs[base + this.a(76, "y", null, "x")]; + break; + } + case 12233: + { + var dst = this.a(null, undefined, [], []); + var a = regs[base + this.a({}, "k", 47, undefined)]; + regs[base + dst] = a in regs[base + this.a(undefined, "q", "k", null)]; + break; + } + case 5881: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this.a({}, {}, [], 56); + var obj = regs[base + this.a(55, {}, null, 1)]; + regs[base + dst] = obj instanceof regs[base + this.a(undefined, {}, {}, 98)]; + break; + } + + // Unary (dst, src) + case 23545: + { + var dst = this.a({}, {}, 88, {}); + regs[base + dst] = -regs[base + this.a({}, 78, undefined, null)]; + break; + } + case 42420: + { + var dst = this.a([], "z", undefined, 46); + regs[base + dst] = +regs[base + this.a(85, null, undefined, undefined)]; + break; + } + case 25347: + { + var dst = this.a({}, 70, null, "x"); + regs[base + dst] = !regs[base + this.a({}, {}, {}, null)]; + break; + } + case 7367: + { + var dst = this.a({}, "k", 74, []); + regs[base + dst] = ~regs[base + this.a([], undefined, [], {})]; + break; + } + case 42831: + { + var dst = this.a(8, {}, undefined, []); + regs[base + dst] = typeof regs[base + this.a(undefined, 51, [], [])]; + break; + } + case 41502: + { + var dst = this.a([], null, [], null); + this.a(80, "q", [], undefined); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case 62003: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this.a([], undefined, [], "y"); + var name = this.G(); + var val = Object.prototype.hasOwnProperty.call(this.f, name) ? this.f[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case 19772: + regs[fp + 4] = this.a(undefined, null, 53, undefined); + break; + case 55595: + { + var src = this.a(null, undefined, "q", undefined); + var target = this.a("q", [], {}, {}); + if (!regs[base + src]) regs[fp + 4] = target; + break; + } + case 25181: + { + // || short-circuit: if truthy, jump over RHS. + var src = this.a([], 30, "k", 23); + var target = this.a({}, [], {}, undefined); + if (regs[base + src]) regs[fp + 4] = target; + break; + } + + // Calls + case 45847: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this.a("k", null, [], null); + var callee = regs[base + this.a(null, [], "k", {})]; + var argc = this.a([], [], [], null); + var args; + if (argc === 2617719534) { + args = regs[base + this.a(null, 94, "k", {})]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.a([], 39, null, {})]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.Q(dst << 1, [], this.f, 91, args, closure, "y"); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case 6030: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.a({}, [], "k", 15); + var receiver = regs[base + this.a(null, "k", [], 45)]; + var callee = regs[base + this.a(null, null, "z", null)]; + var argc = this.a(80, [], null, undefined); + var args; + if (argc === 2617719534) { + args = regs[base + this.a(undefined, "z", "x", [])]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.a(null, undefined, 58, undefined)]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this.Q(dst << 1, 40, receiver, {}, args, closure, {}); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case 51590: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this.a({}, "k", {}, null); + var callee = regs[base + this.a(null, {}, [], 98)]; + var argc = this.a("z", "z", {}, {}); + var args; + if (argc === 2617719534) { + args = regs[base + this.a(58, "z", undefined, null)]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this.a(undefined, {}, "q", "x")]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this.Q(dst << 1 | 1, 70, Object.create(closure.prototype || null), null, args, closure, null); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case 4387: + { + var retVal = regs[base + this.a(undefined, undefined, null, {})]; + this.P(96, "y", fp); // must happen before frame is abandoned + + var caller = regs[fp + 13]; + var retDst = regs[fp + 2]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + 3]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + 15]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this.S = fp; + this.m = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + 10] + (retDst >> 1) : 0] = retVal; + break; + } + case 6897: + throw regs[base + this.a(undefined, null, "y", {})]; + + // Closures + case 63522: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this.a({}, [], "k", 56); + var startPc = this.a(null, 73, {}, {}); + var paramCount = this.a(48, 32, null, []); + var regCount = this.a(undefined, {}, 94, null); + var uvCount = this.a({}, undefined, [], undefined); + var hasRest = this.a(null, undefined, {}, "z"); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this.a(null, {}, 55, 41); + var uvIndex = this.a([], null, [], null); + uvDescs[i] = { + s: isLocalRaw, + o: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + V: paramCount, + l: regCount, + H: startPc, + ae: hasRest + }; + var closure = new Closure(fn, 68); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.s) { + closure.ad.push(this.W(uvd.o, [], null, undefined, fp)); + } else { + closure.ad.push(regs[fp + 8].ad[uvd.o]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.b, self.j, null, self.f).I(c, undefined, this == null ? self.f : this, null, [], "y", Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case 57913: + { + // dst, count, [elemReg...] + var dst = this.a(undefined, {}, "x", null); + var count = this.a(undefined, [], undefined, []); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this.a({}, null, null, [])]; + regs[base + dst] = elems; + break; + } + case 51230: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this.a(34, "x", null, null); + var pairCount = this.a([], null, "y", []); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this.a(54, [], "y", 15)]; + var val = regs[base + this.a(undefined, 8, "x", null)]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case 50853: + { + // obj, key, fn + var obj = regs[base + this.a(40, {}, undefined, {})]; + var key = regs[base + this.a(89, null, "q", undefined)]; + var getterFn = regs[base + this.a([], "q", {}, {})]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case 9477: + { + // obj, key, fn + var obj = regs[base + this.a({}, {}, "y", 10)]; + var key = regs[base + this.a("y", {}, {}, {})]; + var setterFn = regs[base + this.a({}, [], "k", "y")]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case 48242: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this.a(undefined, "y", [], {}); + var obj = regs[base + this.a({}, "q", null, {})]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + x: keys, + O: 0 + }; + break; + } + case 39882: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this.a(undefined, [], {}, undefined); + var iter = regs[base + this.a(44, 1, [], [])]; + var exitTarget = this.a(4, [], [], "y"); + if (iter.O >= iter.x.length) { + regs[fp + 4] = exitTarget; + } else { + regs[base + dst] = iter.x[iter.O++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case 60503: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + Y: this.a({}, "y", undefined, []), + aa: this.a(97, 10, [], []) + }); + break; + } + case 10682: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + 5].pop(); + break; + } + case 34663: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + 5]; + if (!hs) regs[fp + 5] = hs = []; + hs.push({ + A: this.a({}, {}, 52, null), + C: this.a(95, [], 72, "q"), + q: this.a(6, [], "x", null), + N: this.a({}, {}, {}, null) + }); + break; + } + + // Self-modifying bytecode + case 19927: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this.a("k", undefined, 79, undefined); + var sliceStart = this.a(null, {}, undefined, undefined); + var sliceEnd = this.a("y", 66, {}, {}); + var pk = this.a([], undefined, null, undefined) ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.b[destPc + (pi - sliceStart)] = (this.b[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case 41819: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + 4] = regs[base + this.a({}, undefined, [], 97)]; + break; + } + case 49100: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + 4] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this.m; + while (searchFrame) { + var handlers = regs[searchFrame + 5]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this.P("z", undefined, searchFrame); + this.S = searchFrame; + searchFrame = regs[searchFrame + 13]; + this.m = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + 5].pop(); + var hBase = regs[handledFrame + 10]; + if (h.aa !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.aa] = err; + regs[handledFrame + 4] = h.Y; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.C] = h.N; + regs[hBase + h.q] = err; + regs[handledFrame + 4] = h.A; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this.S = handledFrame + regs[handledFrame + 15]; + this.m = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this.m) return this.p[0]; + } + this.w = fake_30; +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, undefined, globals); +vm.I(new Closure({ + V: 0, + l: 9, + H: 0 +}, {}), null, undefined, null, undefined, [], null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-191.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-191.js new file mode 100644 index 00000000..af1d9e37 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-191.js @@ -0,0 +1,18 @@ +function z(a,f,g){this.e=g;this.m=f;this.b=a;this.p=null;this.g=[];this.h=7;this.s=0}var A=new WeakMap;function B(a){this.F=a;this.x=[];this.prototype={}}function da(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})}function ea(a,f){this.g=a;this.j=f;this.l=!1;this.E=[];this.z=void 0} +function C(a,f,g,b,k){for(var e=b.F,l=a.g,m=a.h,c=11+e.i,d=m+c;l.length>>13)&65535);return f}function fa(a){a.r="q";a.k=void 0;return a.l?a.z:a.g[a.j]}function P(a,f){var g=a.p;if(g){var b=a.g;a=f+b[f+7];for(f=b[f+2];f=a.e.length)break;b[e+10]=l+1;var m=a.e[l];b[e+6]=b[e+6]^l|0;try{var c=b[e+2];switch(m){case 2404:var d=a.d(null);b[c+d]=I(a);break;case 47031:d=a.d([]);b[c+d]=a.d([]);break;case 37139:d=a.d(null);var p=I(a);if(!(p in a.b))throw new ReferenceError(`${p} is not defined`);b[c+d]=a.b[p];break;case 41872:d=a.d(void 0);b[c+d]=fa(b[e+1].x[a.d("k")]);break;case 36065:d=a.d(void 0);b[c+d]=b[e+8];break;case 20288:d=a.d(void 0); +b[c+d]=b[c+a.d({})];break;case 50365:a.b[I(a)]=b[c+a.d("k")];break;case 17862:var ja=a.d("k");ha(b[e+1].x[ja],b[c+a.d([])]);break;case 18366:d=a.d(void 0);var n=b[c+a.d(void 0)],q=b[c+a.d({})];b[c+d]=n[q];break;case 15022:n=b[c+a.d(84)];q=b[c+a.d(void 0)];var w=b[c+a.d({})];Reflect.set(n,q,w);break;case 32124:d=a.d(void 0);n=b[c+a.d([])];q=b[c+a.d("z")];b[c+d]=delete n[q];break;case 3623:d=a.d("z");var h=b[c+a.d(73)];b[c+d]=h+b[c+a.d([])];break;case 10183:d=a.d(81);h=b[c+a.d(void 0)];b[c+d]=h-b[c+ +a.d({})];break;case 7357:d=a.d([]);h=b[c+a.d("x")];b[c+d]=h*b[c+a.d({})];break;case 54961:d=a.d("z");h=b[c+a.d(void 0)];b[c+d]=h/b[c+a.d("x")];break;case 21305:d=a.d(62);h=b[c+a.d(49)];b[c+d]=h%b[c+a.d("z")];break;case 20159:d=a.d(void 0);h=b[c+a.d({})];b[c+d]=Math.pow(h,b[c+a.d("k")]);break;case 13292:d=a.d(null);h=b[c+a.d({})];b[c+d]=h&b[c+a.d({})];break;case 57867:d=a.d(void 0);h=b[c+a.d(24)];b[c+d]=h|b[c+a.d([])];break;case 60028:d=a.d(null);h=b[c+a.d(null)];b[c+d]=h^b[c+a.d(76)];break;case 16709:d= +a.d(10);h=b[c+a.d([])];b[c+d]=h<>b[c+a.d(void 0)];break;case 56840:d=a.d(85);h=b[c+a.d({})];b[c+d]=h>>>b[c+a.d(68)];break;case 18329:d=a.d(37);h=b[c+a.d({})];b[c+d]=hb[c+a.d(15)];break;case 43634:d=a.d([]);h=b[c+a.d(void 0)];b[c+d]=h<=b[c+a.d(54)];break;case 17626:d=a.d(void 0);h=b[c+a.d([])];b[c+d]=h>=b[c+a.d(void 0)];break;case 49949:d=a.d(void 0);h=b[c+a.d(null)]; +b[c+d]=h===b[c+a.d("k")];break;case 5541:d=a.d({});h=b[c+a.d(void 0)];b[c+d]=h!==b[c+a.d(void 0)];break;case 24760:d=a.d([]);h=b[c+a.d("q")];b[c+d]=h==b[c+a.d(void 0)];break;case 39806:d=a.d(null);h=b[c+a.d({})];b[c+d]=h!=b[c+a.d(51)];break;case 9427:d=a.d(void 0);h=b[c+a.d(void 0)];b[c+d]=h in b[c+a.d("x")];break;case 38412:d=a.d(50);n=b[c+a.d(void 0)];b[c+d]=n instanceof b[c+a.d(null)];break;case 24086:d=a.d("k");b[c+d]=-b[c+a.d(84)];break;case 14554:d=a.d(void 0);b[c+d]=+b[c+a.d(void 0)];break; +case 2552:d=a.d(null);b[c+d]=!b[c+a.d([])];break;case 23691:d=a.d(void 0);b[c+d]=~b[c+a.d("k")];break;case 48768:d=a.d("k");b[c+d]=typeof b[c+a.d({})];break;case 19131:d=a.d({});a.d([]);b[c+d]=void 0;break;case 64685:d=a.d(51);var S=I(a);w=Object.prototype.hasOwnProperty.call(a.b,S)?a.b[S]:void 0;b[c+d]=typeof w;break;case 53096:b[e+10]=a.d("y");break;case 49894:var J=a.d([]),K=a.d(0);b[c+J]||(b[e+10]=K);break;case 21205:J=a.d([]);K=a.d(26);b[c+J]&&(b[e+10]=K);break;case 55437:d=a.d(7);var r=b[c+ +a.d(void 0)],t=a.d(void 0);if(t===1265226110)g=b[c+a.d(void 0)];else for(g=Array(t),k=0;k>1):0]=D;break;case 52724:throw b[c+a.d([])];case 35774:d=a.d("y");var la=a.d("z"),ma=a.d(null),na=a.d({}),V=a.d(null),oa=a.d([]),M=Array(V);for(k=0;k=G.f.length?b[e+10]=ua:b[c+d]=G.f[G.o++];break;case 43507:var v=b[e+4];v||(b[e+4]=v=[]);v.push({v:a.d([]),t:a.d("q")});break;case 25651:b[e+4].pop();break;case 19248:(v=b[e+4])||(b[e+4]= +v=[]);v.push({B:a.d(null),n:a.d({}),Q:a.d(null),A:a.d(void 0)});break;case 46991:var ba=a.d({}),ca=a.d(null),va=a.d("k"),H=a.d([])^ba|0;for(e=ca;e>>13)>>>0;break;case 62264:b[e+10]=b[c+a.d(48)];break;case 35145:debugger;break;default:throw Error("Unknown opcode: "+m+" at pc "+(b[e+10]-1));}}catch(x){e=0;for(l=a.s;l;){if((m=b[l+4])&&m.length>0){e=l;break}P(a,l);a.h=l;l=b[l+5];a.s=l}if(!e)throw x;l=b[e+4].pop();m=b[e+2];l.t!==void 0?(b[m+l.t]=x,b[e+ +10]=l.v):(b[m+l.n]=l.A,b[m+l.Q]=x,b[e+10]=l.B);a.h=e+b[e+7];a.s=e}if(!a.s)return a.g[0]}}var R=globalThis;typeof window!=="undefined"&&(R.window=window,R.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(R.module=module,R.exports=typeof exports!=="undefined"?exports:void 0); +Q(new z(R,[2,5,"window","TEST_OUTPUT","concat",void 0],[36065,1,35774,4,68,2,5,0,0,20288,2,4,2404,4,0,0,2404,5,1,0,53940,6,2,4,5,20288,3,6,37139,4,2,0,2404,5,3,0,53940,6,0,2404,7,4,0,18366,8,6,7,16460,7,6,8,1,3,55437,6,2,1265226110,7,15022,4,5,6,2404,4,5,0,54001,4,36065,3,3623,4,0,1,54001,4,2404,4,5,0,54001,4]),new B({c:0,i:9,u:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-192.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-192.js new file mode 100644 index 00000000..b28737db --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-192.js @@ -0,0 +1,19 @@ +function x(a){this.G=a;this.l=[];this.prototype={}}function B(a,f,g,b,e){this.i=e;this.D=f;this.k=g;this.u=null;this.g=[];this.a=[a,b];this.j=2;this.h=0}function ea(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(f){return f.charCodeAt(0)})}var C=new WeakMap;function D(a,f){this.g=f;this.o=a;this.q=!1;this.v=void 0} +function E(a,f,g,b,e,k){for(var l=e.G,m=a.g,c=a.j,d=10+l.m,u=c+d;m.length>>13)&65535);return f}function fa(a){a.c=void 0;return a.q?a.v:a.g[a.o]}B.prototype.p=function(){return this.i[this.g[this.h+8]++]};B.prototype.z=function(a,f,g,b,e,k){a=this.g[k+7]+f;f=this.u||(this.u=[]);return f[a]||(f[a]=new D(a,this.g))}; +D.prototype.s=function(a,f){this.q?this.v=f:this.g[this.o]=f;this.w=a};B.prototype.y=function(a,f,g,b){if(f=this.u){var e=this.g;this.f=b;b=g+e[g+9];this.n=a;for(a=e[g+7];a=l.length)break;b[e+8]=m+1;m=a.i[m];b[e+5]=(b[e+5]+1)%l.length;try{var c=b[e+7];switch(m){case 5311:var d=a.p({},null);b[c+d]=Q(a);break;case 29785:d=a.p(void 0,{});b[c+d]=a.p([],14);break;case 41170:d=a.p(89,"x");var u=Q(a);if(!(u in a.k))throw new ReferenceError(`${u} is not defined`);b[c+d]=a.k[u];break;case 56573:d=a.p("q",null);b[c+d]=fa(b[e+4].l[a.p("y",null)]);break;case 56670:d=a.p([],void 0);b[c+ +d]=b[e+2];break;case 44637:d=a.p(42,"y");b[c+d]=b[c+a.p({},void 0)];break;case 20843:a.k[Q(a)]=b[c+a.p({},[])];break;case 2347:var q=a.p("y",{});b[e+4].l[q].s(19,b[c+a.p([],[])]);break;case 53667:d=a.p(37,49);var n=b[c+a.p({},"x")],p=b[c+a.p(null,[])];b[c+d]=n[p];break;case 54165:n=b[c+a.p(void 0,void 0)];p=b[c+a.p(null,[])];var y=b[c+a.p([],{})];Reflect.set(n,p,y);break;case 10406:d=a.p(null,65);n=b[c+a.p([],void 0)];p=b[c+a.p(43,[])];b[c+d]=delete n[p];break;case 27840:d=a.p({},"z");var h=b[c+a.p(null, +{})];b[c+d]=h+b[c+a.p(void 0,"q")];break;case 934:d=a.p(void 0,"z");h=b[c+a.p(null,[])];b[c+d]=h-b[c+a.p("q","q")];break;case 22512:d=a.p("z",82);h=b[c+a.p(74,[])];b[c+d]=h*b[c+a.p("k",{})];break;case 38776:d=a.p([],[]);h=b[c+a.p("y","k")];b[c+d]=h/b[c+a.p(0,{})];break;case 16591:d=a.p({},null);h=b[c+a.p(void 0,{})];b[c+d]=h%b[c+a.p([],void 0)];break;case 22715:d=a.p([],null);h=b[c+a.p(66,null)];b[c+d]=Math.pow(h,b[c+a.p([],null)]);break;case 40046:d=a.p(null,[]);h=b[c+a.p("x","x")];b[c+d]=h&b[c+ +a.p(32,void 0)];break;case 16840:d=a.p({},void 0);h=b[c+a.p(72,[])];b[c+d]=h|b[c+a.p(54,80)];break;case 35019:d=a.p(63,[]);h=b[c+a.p({},[])];b[c+d]=h^b[c+a.p(void 0,{})];break;case 12158:d=a.p(null,[]);h=b[c+a.p("z",[])];b[c+d]=h<>b[c+a.p(void 0,void 0)];break;case 63201:d=a.p(null,[]);h=b[c+a.p(null,[])];b[c+d]=h>>>b[c+a.p({},5)];break;case 35824:d=a.p([],null);h=b[c+a.p([],{})];b[c+d]=hb[c+a.p(void 0,{})];break;case 44681:d=a.p(18,74);h=b[c+a.p({},null)];b[c+d]=h<=b[c+a.p(void 0,void 0)];break;case 41007:d=a.p(null,void 0);h=b[c+a.p({},"x")];b[c+d]=h>=b[c+a.p({},null)];break;case 34701:d=a.p(void 0,[]);h=b[c+a.p(null,null)];b[c+d]=h===b[c+a.p(void 0,void 0)];break;case 53970:d=a.p(32,null);h=b[c+a.p([],{})];b[c+d]=h!==b[c+a.p("k",[])];break;case 28535:d=a.p({},"y");h=b[c+a.p(73,{})];b[c+d]=h==b[c+a.p("z",{})];break;case 35900:d=a.p(86, +"k");h=b[c+a.p("y","z")];b[c+d]=h!=b[c+a.p(null,"k")];break;case 64062:d=a.p(null,22);h=b[c+a.p(void 0,{})];b[c+d]=h in b[c+a.p(87,"z")];break;case 10154:d=a.p("q",[]);n=b[c+a.p({},null)];b[c+d]=n instanceof b[c+a.p(void 0,"k")];break;case 54307:d=a.p(null,"z");b[c+d]=-b[c+a.p({},void 0)];break;case 13754:d=a.p({},4);b[c+d]=+b[c+a.p([],"q")];break;case 29330:d=a.p({},void 0);b[c+d]=!b[c+a.p("y","z")];break;case 54688:d=a.p(void 0,83);b[c+d]=~b[c+a.p("y",{})];break;case 63267:d=a.p(null,"q");b[c+d]= +typeof b[c+a.p(void 0,[])];break;case 38546:d=a.p([],45);a.p("k",null);b[c+d]=void 0;break;case 62836:d=a.p(void 0,78);var T=Q(a);y=Object.prototype.hasOwnProperty.call(a.k,T)?a.k[T]:void 0;b[c+d]=typeof y;break;case 60656:b[e+8]=a.p(5,null);break;case 22172:var K=a.p(null,[]),L=a.p(null,[]);b[c+K]||(b[e+8]=L);break;case 31454:K=a.p([],"z");L=a.p({},[]);b[c+K]&&(b[e+8]=L);break;case 33160:d=a.p([],void 0);var r=b[c+a.p(62,void 0)],t=a.p({},void 0);if(t===3208792843)g=b[c+a.p({},11)];else for(g=Array(t), +k=0;k>1):0]=F;break;case 42135:throw b[c+a.p(null,{})];case 1894:d=a.p({},"q");var ia=a.p({},{}),ja=a.p({},null),ka=a.p(null,void 0),W=a.p({},27),la=a.p(33,43),N=Array(W);for(k=0;k=I.B.length?b[e+8]=ra:b[c+d]=I.B[I.e++];break; +case 60149:var w=b[e+0];w||(b[e+0]=w=[]);w.push({Q:a.p("x",{}),C:a.p(null,null)});break;case 54011:b[e+0].pop();break;case 34588:(w=b[e+0])||(b[e+0]=w=[]);w.push({H:a.p(null,{}),I:a.p("x",{}),J:a.p("x",73),F:a.p({},[])});break;case 11569:var ca=a.p("y","z"),da=a.p([],[]),sa=a.p(null,"z"),J=a.p("z",void 0)^ca|0;for(e=da;e>>13)>>>0;break;case 36150:b[e+8]=b[c+a.p("q",73)];break;case 15003:debugger;break;default:throw Error("Unknown opcode: "+m+" at pc "+ +(b[e+8]-1));}}catch(z){e=0;for(l=a.h;l;){if((m=b[l+0])&&m.length>0){e=l;break}a.y("q",{},l,42);a.j=l;l=b[l+3];a.h=l}if(!e)throw z;l=b[e+0].pop();m=b[e+7];l.C!==void 0?(b[m+l.C]=z,b[e+8]=l.Q):(b[m+l.I]=l.F,b[m+l.J]=z,b[e+8]=l.H);a.j=e+b[e+9];a.h=e}if(!a.h)return a.g[0]}}var S=globalThis;typeof window!=="undefined"&&(S.window=window,S.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(S.module=module,S.exports=typeof exports!=="undefined"?exports:void 0); +R(new B(null,[2,5,"window","TEST_OUTPUT","concat",void 0],S,{},[56670,1,1894,4,68,2,5,0,0,44637,2,4,5311,4,0,0,5311,5,1,0,52197,6,2,4,5,44637,3,6,41170,4,2,0,5311,5,3,0,52197,6,0,5311,7,4,0,53667,8,6,7,44094,7,6,8,1,3,33160,6,2,3208792843,7,54165,4,5,6,5311,4,5,0,33142,4,56670,3,27840,4,0,1,33142,4,5311,4,5,0,33142,4]),new x({d:0,m:9,A:0}),null); diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-193.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-193.js new file mode 100644 index 00000000..d50161bc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread-minified-193.js @@ -0,0 +1,19 @@ +function w(e,a){this.r=a;this.I=e;this.w=[];this.prototype={}}function A(e,a){this.g=a;this.v=e;this.o=!1;this.u=void 0}function da(e){return typeof Buffer!=="undefined"?Buffer.from(e,"base64"):Uint8Array.from(atob(e),function(a){return a.charCodeAt(0)})}var B=new WeakMap;function C(e,a,g,d){this.j=d;this.F=g;this.t=a;this.i=e;this.A=null;this.g=[];this.l=6;this.h=0}C.prototype.c=function(e,a,g,d){e=this.g[d+6]+a;a=this.A||(this.A=[]);this.y=g;return a[e]||(a[e]=new A(e,this.g))}; +A.prototype.x=function(e,a,g){this.f=g;this.o?this.u=e:this.g[this.v]=e};C.prototype.d=function(){return this.j[this.g[this.h+2]++]};function D(e){var a=e.d([]),g=e.d(12);e=e.F[a];if(!g)return e;if(typeof e==="number")return e^g;if(typeof e!=="string")return e;e=da(e);a="";for(var d=0;d>>13)&65535);return a}function ea(e){return e.o?e.u:e.g[e.v]} +function P(e,a,g,d,f,k,b){for(var c=d.I,l=e.g,p=e.l,m=15+c.m,n=p+m;l.length=this.j.length)break;a[d+2]=k+1;k=this.j[k];a[d+7]++;try{var b=a[d+6];switch(k){case 48173:var c=this.d([]);a[b+c]=D(this);break;case 165:c=this.d(null);a[b+c]=this.d({});break;case 40683:c=this.d({});var l=D(this);if(!(l in this.i))throw new ReferenceError(`${l} is not defined`);a[b+c]=this.i[l];break;case 64726:c=this.d("z");a[b+c]=ea(a[d+11].w[this.d([])]);break;case 32591:c=this.d(null); +a[b+c]=a[d+0];break;case 54717:c=this.d(48);a[b+c]=a[b+this.d({})];break;case 4141:this.i[D(this)]=a[b+this.d(null)];break;case 54656:var p=this.d({});a[d+11].w[p].x(a[b+this.d(22)],{},44,[]);break;case 20296:c=this.d({});var m=a[b+this.d(void 0)],n=a[b+this.d(void 0)];a[b+c]=m[n];break;case 40621:m=a[b+this.d(51)];n=a[b+this.d(void 0)];var x=a[b+this.d(void 0)];Reflect.set(m,n,x);break;case 728:c=this.d(89);m=a[b+this.d(void 0)];n=a[b+this.d({})];a[b+c]=delete m[n];break;case 12927:c=this.d(void 0); +var h=a[b+this.d({})];a[b+c]=h+a[b+this.d([])];break;case 54142:c=this.d(27);h=a[b+this.d(86)];a[b+c]=h-a[b+this.d({})];break;case 28617:c=this.d([]);h=a[b+this.d("y")];a[b+c]=h*a[b+this.d([])];break;case 13717:c=this.d(null);h=a[b+this.d([])];a[b+c]=h/a[b+this.d(54)];break;case 12907:c=this.d(92);h=a[b+this.d({})];a[b+c]=h%a[b+this.d("z")];break;case 20742:c=this.d("q");h=a[b+this.d("k")];a[b+c]=Math.pow(h,a[b+this.d([])]);break;case 23382:c=this.d({});h=a[b+this.d("x")];a[b+c]=h&a[b+this.d({})]; +break;case 55577:c=this.d(51);h=a[b+this.d("y")];a[b+c]=h|a[b+this.d(void 0)];break;case 47611:c=this.d([]);h=a[b+this.d([])];a[b+c]=h^a[b+this.d(void 0)];break;case 32957:c=this.d("x");h=a[b+this.d(74)];a[b+c]=h<>a[b+this.d(void 0)];break;case 63987:c=this.d(void 0);h=a[b+this.d(79)];a[b+c]=h>>>a[b+this.d(null)];break;case 34619:c=this.d("y");h=a[b+this.d([])];a[b+c]=ha[b+this.d(80)];break;case 46904:c=this.d(49);h=a[b+this.d([])];a[b+c]=h<=a[b+this.d(26)];break;case 41281:c=this.d([]);h=a[b+this.d({})];a[b+c]=h>=a[b+this.d({})];break;case 28815:c=this.d(null);h=a[b+this.d({})];a[b+c]=h===a[b+this.d(void 0)];break;case 48755:c=this.d(void 0);h=a[b+this.d("q")];a[b+c]=h!==a[b+this.d(null)];break;case 63420:c=this.d({});h=a[b+this.d(null)];a[b+c]=h==a[b+this.d("k")];break;case 55099:c=this.d(void 0);h=a[b+this.d(null)];a[b+c]=h!=a[b+this.d([])]; +break;case 25677:c=this.d("q");h=a[b+this.d([])];a[b+c]=h in a[b+this.d(26)];break;case 46221:c=this.d([]);m=a[b+this.d("k")];a[b+c]=m instanceof a[b+this.d([])];break;case 60250:c=this.d(null);a[b+c]=-a[b+this.d([])];break;case 46577:c=this.d("z");a[b+c]=+a[b+this.d(null)];break;case 6263:c=this.d(void 0);a[b+c]=!a[b+this.d(44)];break;case 64253:c=this.d(null);a[b+c]=~a[b+this.d([])];break;case 43251:c=this.d([]);a[b+c]=typeof a[b+this.d("k")];break;case 20308:c=this.d([]);this.d([]);a[b+c]=void 0; +break;case 5179:c=this.d("y");var S=D(this);x=Object.prototype.hasOwnProperty.call(this.i,S)?this.i[S]:void 0;a[b+c]=typeof x;break;case 16135:a[d+2]=this.d("y");break;case 26982:var J=this.d([]),K=this.d([]);a[b+J]||(a[d+2]=K);break;case 36219:J=this.d(null);K=this.d({});a[b+J]&&(a[d+2]=K);break;case 63:c=this.d([]);var q=a[b+this.d({})],r=this.d("z");if(r===1330407607)g=a[b+this.d(83)];else for(g=Array(r),f=0;f>1):0]=E;break;case 25297:throw a[b+this.d([])];case 35054:c=this.d([]);var ha=this.d("x"),ia=this.d("y"),ja=this.d("q"),V=this.d({}),ka=this.d(null),M=Array(V);for(f=0;f=H.z.length?a[d+2]=qa:a[b+c]=H.z[H.b++];break;case 48497:var u=a[d+3];u||(a[d+3]=u=[]);u.push({H:this.d(null),C:this.d({})});break;case 5166:a[d+3].pop(); +break;case 3456:(u=a[d+3])||(a[d+3]=u=[]);u.push({K:this.d([]),E:this.d("k"),J:this.d([]),M:this.d(92)});break;case 27348:var ba=this.d({}),ca=this.d(void 0),ra=this.d(null),I=this.d({})^ba|0;for(d=ca;d>>13)>>>0;break;case 24842:a[d+2]=a[b+this.d({})];break;case 31048:debugger;break;default:throw Error("Unknown opcode: "+k+" at pc "+(a[d+2]-1));}}catch(y){d=0;for(k=this.h;k;){var v=a[k+3];if(v&&v.length>0){d=k;break}Q(this,k,{});this.l=k;this.h= +k=a[k+12]}if(!d)throw y;k=a[d+3].pop();v=a[d+6];k.C!==void 0?(a[v+k.C]=y,a[d+2]=k.H):(a[v+k.E]=k.M,a[v+k.J]=y,a[d+2]=k.K);this.l=d+a[d+8];this.h=d}if(!this.h)return this.g[0]}};var R=globalThis;typeof window!=="undefined"&&(R.window=window,R.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(R.module=module,R.exports=typeof exports!=="undefined"?exports:void 0); +(new C(R,[],[2,5,"window","TEST_OUTPUT","concat",void 0],[32591,1,35054,4,68,2,5,0,0,54717,2,4,48173,4,0,0,48173,5,1,0,13063,6,2,4,5,54717,3,6,40683,4,2,0,48173,5,3,0,13063,6,0,48173,7,4,0,20296,8,6,7,48692,7,6,8,1,3,63,6,2,1330407607,7,40621,4,5,6,48173,4,5,0,39588,4,32591,3,12927,4,0,1,39588,4,48173,4,5,0,39588,4])).s(new w({Q:0,m:9,D:0},"z"),void 0,null,"q"); diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/spread.baseline.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread.baseline.js new file mode 100644 index 00000000..3443d098 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread.baseline.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */2, /* 1 */5, /* 2 */"window", /* 3 */"TEST_OUTPUT", /* 4 */"concat", /* 5 */undefined]; +var BYTECODE = [4, 1, 47, 4, 68, 2, 5, 0, 0, 5, 2, 4, 0, 4, 0, 0, 0, 5, 1, 0, 48, 6, 2, 4, 5, 5, 3, 6, 2, 4, 2, 0, 0, 5, 3, 0, 48, 6, 0, 0, 7, 4, 0, 8, 8, 6, 7, 43, 7, 6, 8, 1, 3, 42, 6, 2, 65535, 7, 9, 4, 5, 6, 0, 4, 5, 0, 45, 4, 4, 3, 11, 4, 0, 1, 45, 4, 0, 4, 5, 0, 45, 4]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-class/spread.source.js b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread.source.js new file mode 100644 index 00000000..6eeaf0d5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-class/spread.source.js @@ -0,0 +1 @@ +function sum(a, b) { return a + b; } const args = [2, 5]; window.TEST_OUTPUT = sum(...args); diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/encoded.js new file mode 100644 index 00000000..798a8a9e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/encoded.js @@ -0,0 +1,1021 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */"finished", /* 5 */undefined, /* 6 */0, /* 7 */"Error", /* 8 */"negative", /* 9 */"message", /* 10 */true]; +var BYTECODE = [24403, 1, 48708, 3, 69, 1, 9, 0, 0, 27275, 2, 3, 16425, 3, 0, 0, 34323, 4, 1, 0, 34323, 5, 2, 0, 32486, 6, 5, 54995, 5, 2, 1, 6, 34323, 6, 3, 0, 54995, 7, 2, 1, 6, 16425, 6, 0, 0, 34323, 8, 4, 0, 34633, 9, 6, 8, 46311, 6, 3, 5, 7, 9, 8214, 3, 4, 6, 34323, 3, 5, 0, 55982, 3, 24403, 2, 49368, 149, 4, 5, 167, 1831, 126, 3, 34323, 6, 6, 0, 55171, 7, 0, 6, 17385, 7, 105, 16425, 6, 7, 0, 34323, 7, 8, 0, 22997, 8, 6, 1, 7, 23852, 8, 34323, 6, 2, 0, 1324, 7, 0, 6, 50307, 27275, 5, 7, 60758, 4, 169, 50307, 45094, 149, 50307, 45094, 143, 34323, 6, 9, 0, 34633, 7, 3, 6, 27275, 5, 7, 60758, 4, 171, 50307, 45094, 149, 50307, 60758, 4, 173, 45094, 149, 16425, 6, 0, 0, 34323, 7, 4, 0, 34323, 8, 10, 0, 8214, 6, 7, 8, 61977, 4, 23852, 5, 55982, 5, 55982, 5, 34323, 6, 5, 0, 55982, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 34323, + LOAD_INT: 60758, + LOAD_GLOBAL: 16425, + LOAD_UPVALUE: 3899, + LOAD_THIS: 24403, + MOVE: 27275, + STORE_GLOBAL: 22683, + STORE_UPVALUE: 3174, + GET_PROP: 34633, + SET_PROP: 8214, + DELETE_PROP: 14618, + ADD: 1324, + SUB: 33238, + MUL: 33337, + DIV: 37740, + MOD: 19063, + EXP: 38671, + BAND: 31131, + BOR: 34923, + BXOR: 3056, + SHL: 20147, + SHR: 60733, + USHR: 22400, + LT: 55171, + GT: 36968, + LTE: 61788, + GTE: 53954, + EQ: 32483, + NEQ: 50055, + LOOSE_EQ: 41341, + LOOSE_NEQ: 22612, + IN: 47966, + INSTANCEOF: 12578, + UNARY_NEG: 32486, + UNARY_POS: 44804, + UNARY_NOT: 31834, + UNARY_BITNOT: 34144, + TYPEOF: 5596, + VOID: 42458, + TYPEOF_SAFE: 18543, + JUMP: 45094, + JUMP_IF_FALSE: 17385, + JUMP_IF_TRUE: 33463, + CALL: 54995, + CALL_METHOD: 21386, + NEW: 22997, + RETURN: 55982, + THROW: 23852, + MAKE_CLOSURE: 48708, + BUILD_ARRAY: 46311, + BUILD_OBJECT: 51997, + DEFINE_GETTER: 4164, + DEFINE_SETTER: 2161, + FOR_IN_SETUP: 43298, + FOR_IN_NEXT: 57182, + TRY_SETUP: 1831, + TRY_END: 50307, + PATCH: 44387, + DEBUGGER: 31189, + JUMP_REG: 61977, + FINALLY_SETUP: 49368 +}; +var SENTINELS = { + CALL_SPREAD: 1250018095 +}; +var SLOTS = { + THIS: 4, + CALLER: 10, + NOISE_END: 7, + CLOSURE: 8, + REG_BASE: 1, + NOISE_PC: 2, + HANDLERS: 0, + FRAME_SIZE: 3, + PC: 5, + RET_DST: 6 +}; +var HEADER_SIZE = 11; +var FRAME_START = 3; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/qualification.json new file mode 100644 index 00000000..bd7336aa --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/qualification.json @@ -0,0 +1,69 @@ +{ + "id": "exceptions--randomizeOpcodes--154", + "case": "exceptions", + "profile": "randomizeOpcodes", + "seed": 154, + "options": { + "randomizeOpcodes": true, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-runtime-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/integration/cli.test.js b/test/vm/jsconfuser-vm/integration/cli.test.js new file mode 100644 index 00000000..a85ab95a --- /dev/null +++ b/test/vm/jsconfuser-vm/integration/cli.test.js @@ -0,0 +1,295 @@ +import crypto from 'node:crypto' +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { expect, test } from 'vitest' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './harness.js' +import { diagnoseStandalone } from '../../../../src/vm/jsconfuser-vm/decode-standalone.js' + +const testDirectory = path.dirname(fileURLToPath(import.meta.url)) +const packageRoot = path.resolve(testDirectory, '../../../..') +const repositoryRoot = packageRoot +const evidenceRoot = fs.mkdtempSync(path.join(tmpdir(), 'jsconfuser-vm-cli-')) +const artifactRoot = path.join(evidenceRoot, 'cli-artifacts') +const successInput = path.join( + repositoryRoot, + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/encoded.js', +) +const legacyInput = path.join( + packageRoot, + 'test/visitor/calculate-constant-exp/non-literal-left.js', +) + +fs.mkdirSync(artifactRoot, { recursive: true }) + +function sha256(source) { + return crypto.createHash('sha256').update(source).digest('hex') +} + +function createCase(name) { + const directory = path.join(artifactRoot, name) + fs.rmSync(directory, { force: true, recursive: true }) + fs.mkdirSync(directory, { recursive: true }) + return directory +} + +function runCli(args) { + const completed = spawnSync(process.execPath, ['src/main.js', ...args], { + cwd: packageRoot, + encoding: 'utf8', + }) + if (completed.error) throw completed.error + return completed +} + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')) +} + +function assertDeclinedAttempt({ input, outputPath, resultPath, completed }) { + expect(completed.status).toBe(0) + expect(completed.stdout).toContain('Status: declined') + expect(completed.stderr).toContain('Diagnostic: container-declined') + + const output = fs.readFileSync(outputPath, 'utf8') + const record = readJson(resultPath) + expect(output).toBe(input) + expect(sha256(output)).toBe(sha256(input)) + expect(record).toMatchObject({ + schemaVersion: 'jsconfuser-vm-adapter.v1', + ok: false, + status: 'declined', + input, + output: input, + result: null, + diagnostic: { + code: 'container-declined', + message: expect.any(String), + }, + }) +} + +test('subprocess success writes fresh output and the explicit adapter result', () => { + const directory = createCase('success-explicit-result') + const outputPath = path.join(directory, 'decoded.js') + const resultPath = path.join(directory, 'decoded.json') + const completed = runCli([ + '-t', + 'jsconfuser-vm', + '-i', + successInput, + '-o', + outputPath, + '--result', + resultPath, + ]) + const input = fs.readFileSync(successInput, 'utf8') + const output = fs.readFileSync(outputPath, 'utf8') + const record = readJson(resultPath) + + expect(completed.status).toBe(0) + expect(completed.stdout).toContain('Status: success') + expect(completed.stdout).toContain(`Output written: ${outputPath}`) + expect(completed.stdout).toContain(`Result written: ${resultPath}`) + expect(completed.stderr).toBe('') + expect(output).not.toBe(input) + expect(record).toMatchObject({ + schemaVersion: 'jsconfuser-vm-adapter.v1', + ok: true, + status: 'success', + input, + output, + diagnostic: null, + result: { + schemaVersion: 'jsconfuser-vm-standalone.v2', + parseOnly: true, + targetExecution: false, + vmExecuted: false, + emittedJavaScript: true, + proof: { + allPredecessorsReconstructed: true, + noTargetExecution: true, + noVmRuntimeEmission: true, + freshJavaScriptEmission: true, + }, + }, + }) + expect(record.output).toBe(output) + const direct = diagnoseStandalone(input) + expect(direct.ok).toBe(true) + expect(record.result).toEqual(direct.result) + + const ast = parseSuccessfulOutput(output) + expect(assertNoVmResidue(ast).ok).toBe(true) + expect(assertDerivedStateConsistency(ast, output).ok).toBe(true) +}) + +test('subprocess ordinary decline writes exact output and the default sidecar result', () => { + const directory = createCase('ordinary-decline-default-result') + const inputPath = path.join(directory, 'input.js') + const outputPath = path.join(directory, 'declined.js') + const resultPath = `${outputPath}.result.json` + const input = 'const answer = 42;\nwindow.TEST_OUTPUT = answer;\n' + fs.writeFileSync(inputPath, input) + + const completed = runCli([ + '-t', + 'jsconfuser-vm', + '-i', + inputPath, + '-o', + outputPath, + ]) + + assertDeclinedAttempt({ input, outputPath, resultPath, completed }) + expect(completed.stdout).toContain(`Result written: ${resultPath}`) +}) + +test('subprocess container decline remains exact through an explicit result path', () => { + const directory = createCase('container-decline-explicit-result') + const inputPath = path.join(directory, 'input.js') + const outputPath = path.join(directory, 'declined.js') + const resultPath = path.join(directory, 'declined.json') + const encoded = fs.readFileSync(successInput, 'utf8') + const input = encoded.replace('var BYTECODE =', 'var NOT_BYTECODE =') + expect(input).not.toBe(encoded) + fs.writeFileSync(inputPath, input) + + const completed = runCli([ + '-t', + 'jsconfuser-vm', + '-i', + inputPath, + '-o', + outputPath, + '--result', + resultPath, + ]) + + assertDeclinedAttempt({ input, outputPath, resultPath, completed }) +}) + +test('rejects a result path that aliases the output before writing either file', () => { + const directory = createCase('result-output-alias') + const outputPath = path.join(directory, 'decoded.js') + const aliasPath = path.join(directory, 'nested', '..', 'decoded.js') + const sentinel = 'output must remain untouched\n' + fs.writeFileSync(outputPath, sentinel) + + const completed = runCli([ + '-t', + 'jsconfuser-vm', + '-i', + successInput, + '-o', + outputPath, + '--result', + aliasPath, + ]) + + expect(completed.status).toBe(1) + expect(completed.stderr).toContain( + 'Output and result files must be different', + ) + expect(fs.readFileSync(outputPath, 'utf8')).toBe(sentinel) +}) + +test('unknown target keeps the established non-throwing error path', () => { + const directory = createCase('unknown-target') + const outputPath = path.join(directory, 'unknown.js') + const resultPath = path.join(directory, 'unknown.json') + const completed = runCli([ + '-t', + 'not-a-target', + '-i', + legacyInput, + '-o', + outputPath, + '--result', + resultPath, + ]) + + expect(completed.status).toBe(1) + expect(completed.stdout).toContain('Type: not-a-target') + expect(completed.stderr).toContain('Unknown type: not-a-target') + expect(fs.existsSync(outputPath)).toBe(false) + expect(fs.existsSync(resultPath)).toBe(false) +}) + +test('missing VM input keeps the established read error path', () => { + const directory = createCase('missing-input') + const inputPath = path.join(directory, 'missing.js') + const outputPath = path.join(directory, 'missing-output.js') + const resultPath = path.join(directory, 'missing-result.json') + const completed = runCli([ + '-t', + 'jsconfuser-vm', + '-i', + inputPath, + '-o', + outputPath, + '--result', + resultPath, + ]) + + expect(completed.status).toBe(1) + expect(completed.stderr).toContain(`Cannot read input file ${inputPath}`) + expect(fs.existsSync(outputPath)).toBe(false) + expect(fs.existsSync(resultPath)).toBe(false) +}) + +test('output write failure keeps the result path unwritten', () => { + const directory = createCase('output-write-failure') + const inputPath = path.join(directory, 'input.js') + const outputPath = path.join(directory, 'missing', 'output.js') + const resultPath = path.join(directory, 'result.json') + fs.writeFileSync(inputPath, 'const answer = 42;\n') + + const completed = runCli([ + '-t', + 'jsconfuser-vm', + '-i', + inputPath, + '-o', + outputPath, + '--result', + resultPath, + ]) + + expect(completed.status).toBe(1) + expect(completed.stderr).toContain(`Cannot write output file ${outputPath}`) + expect(fs.existsSync(outputPath)).toBe(false) + expect(fs.existsSync(resultPath)).toBe(false) +}) + +test('existing common target preserves its pre-change output and no sidecar', () => { + const directory = createCase('legacy-common') + const outputPath = path.join(directory, 'common.js') + const completed = runCli([ + '-t', + 'common', + '-i', + legacyInput, + '-o', + outputPath, + ]) + const output = fs.readFileSync(outputPath, 'utf8') + const baseline = fs.readFileSync( + path.join( + packageRoot, + 'test/vm/jsconfuser-vm/fixtures/legacy-common/common.js', + ), + 'utf8', + ) + + expect(completed.status).toBe(0) + expect(completed.stdout).toContain(`Output written: ${outputPath}`) + expect(output).toBe(baseline) + expect(fs.existsSync(`${outputPath}.result.json`)).toBe(false) +}) diff --git a/test/vm/jsconfuser-vm/integration/debugger-policy.test.js b/test/vm/jsconfuser-vm/integration/debugger-policy.test.js new file mode 100644 index 00000000..7dcac316 --- /dev/null +++ b/test/vm/jsconfuser-vm/integration/debugger-policy.test.js @@ -0,0 +1,82 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import generate from '@babel/generator' +import { expect, test } from 'vitest' +import decodeVm from '../../../../src/plugin/jsconfuser-vm.js' +import { + assertNoVmResidue, + assertDerivedStateConsistency, + parseSuccessfulOutput, +} from './harness.js' + +const fixture = (name) => + fs.readFileSync(new URL(`../fixtures/${name}`, import.meta.url), 'utf8') + +function counts(source) { + const result = { debugger: 0, noOp: 0, dispatchSwitch: 0 } + traverse(parseSuccessfulOutput(source), { + DebuggerStatement() { + result.debugger++ + }, + ExpressionStatement(path) { + if ( + path.node.expression.type === 'UnaryExpression' && + path.node.expression.operator === 'void' + ) + result.noOp++ + }, + SwitchStatement() { + result.dispatchSwitch++ + }, + }) + return result +} + +test.each([ + 'corpus/raw/f-debugger-order/encoded.js', + 'class-runtime-boot/encoded.js', + 'minified-class-boot/encoded-169.js', + 'randomized-class/arithmetic-181.js', + 'randomized-class/arithmetic-minified-191.js', +])('%s: deleting DEBUGGER leaves the same complete recovered output', (name) => { + const source = fixture(name) + const intact = decodeVm(source) + expect(intact.ok, JSON.stringify(intact.diagnostic)).toBe(true) + const ast = parse(source) + let removed = 0 + traverse(ast, { + DebuggerStatement(path) { + removed++ + path.remove() + }, + }) + expect(removed).toBe(1) + const input = generate(ast).code + const record = decodeVm(input) + expect(record.ok, JSON.stringify(record.diagnostic)).toBe(true) + expect(record.output).not.toBe(input) + expect(record.output).toBe(intact.output) + expect(counts(record.output)).toEqual({ + debugger: 0, + noOp: 0, + dispatchSwitch: 0, + }) + const outputAst = parseSuccessfulOutput(record.output) + expect(assertNoVmResidue(outputAst).ok).toBe(true) + expect(assertDerivedStateConsistency(outputAst, record.output).ok).toBe(true) +}) + +test('DEBUGGER wordcode disappears while before/after effects retain their order', () => { + const record = decodeVm(fixture('corpus/raw/f-debugger-order/encoded.js')) + expect(record.ok).toBe(true) + expect(counts(record.output)).toEqual({ + debugger: 0, + noOp: 0, + dispatchSwitch: 0, + }) + const window = {} + vm.runInNewContext(record.output, { window }, { timeout: 1000 }) + expect(Array.from(window.TEST_OUTPUT)).toEqual(['before', 'after']) +}) diff --git a/test/vm/jsconfuser-vm/integration/harness.test.js b/test/vm/jsconfuser-vm/integration/harness.test.js new file mode 100644 index 00000000..3794e954 --- /dev/null +++ b/test/vm/jsconfuser-vm/integration/harness.test.js @@ -0,0 +1,203 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import { + assertDebugCommentIndependent, + assertDerivedStateConsistency, + checkAttempt, + checkVmResidue, + findVmResidue, + loadVmCell, + parseNumericContainer, + parseSuccessfulOutput, + runAttempt, + runBoundedBehavioralOracle, +} from './harness.js' + +const rawCell = loadVmCell('f-literals-order') +const focusedCell = loadVmCell('focused-call-method-spread', { + kind: 'focused', +}) + +test('loads only stable raw/focused comment-stripped numeric fixtures', () => { + expect(rawCell.paths.encoded).toMatch(/encoded\.js$/) + expect(rawCell.paths).not.toHaveProperty('encodedDebug') + expect(rawCell.encodedSource.startsWith('//')).toBe(false) + expect( + rawCell.bytecode.every((word) => Number.isSafeInteger(word) && word >= 0), + ).toBe(true) + expect(focusedCell.corpusKind).toBe('focused') + expect(focusedCell.oracle.cellId).toBe('focused-call-method-spread') + expect(focusedCell.bytecode).toHaveLength(88) +}) + +test('rejects the explicitly declined encoded-bytecode mode', () => { + const encodedInput = focusedCell.encodedSource.replace( + 'var ENCODE_BYTECODE = false;', + 'var ENCODE_BYTECODE = true;', + ) + expect(() => parseNumericContainer(encodedInput)).toThrowError( + expect.objectContaining({ code: 'unsupported-encoded-bytecode' }), + ) +}) + +test('writes and checks named successful attempts, then invokes the oracle seam', async () => { + const oracleCalls = [] + const attempt = await runAttempt(focusedCell, { + behavioralOracle: ({ metadata, recoveredSource, source }, signal) => { + oracleCalls.push({ metadata, recoveredSource, signal, source }) + return { ok: true } + }, + decoder: () => 'window.TEST_OUTPUT = 5;', + name: 'success-with-oracle', + }) + + expect(attempt.status).toBe('success') + expect(attempt.result.attemptName).toBe('success-with-oracle') + expect(attempt.outputPath).toMatch( + /jsconfuser-vm-harness-[^/]+\/attempts\/focused\/focused-call-method-spread\/success-with-oracle\.js$/, + ) + expect(attempt.oracleResult.status).toBe('passed') + expect(oracleCalls).toHaveLength(1) + expect(oracleCalls[0].metadata.expected).toEqual([1, 2, 3, 4, 5]) + expect(checkAttempt(attempt).ok).toBe(true) + expect(fs.readFileSync(attempt.outputPath, 'utf8')).toBe( + 'window.TEST_OUTPUT = 5;', + ) +}) + +test('writes an exact unchanged output for a decline and never invokes the oracle', async () => { + let oracleCalled = false + const attempt = await runAttempt(rawCell, { + behavioralOracle: () => { + oracleCalled = true + return true + }, + decoder: () => null, + name: 'decline-exact', + }) + + expect(attempt.status).toBe('declined') + expect(attempt.output).toBe(rawCell.encodedSource) + expect(attempt.result.oracle.invoked).toBe(false) + expect(oracleCalled).toBe(false) + expect(checkAttempt(attempt).ok).toBe(true) +}) + +test('runs the behavioral seam only after output parsing and structural checks', async () => { + let oracleCalled = false + await expect( + runAttempt(focusedCell, { + behavioralOracle: () => { + oracleCalled = true + return true + }, + decoder: () => `${focusedCell.encodedSource}\nvar partial = true;`, + name: 'partial-recovery', + }), + ).rejects.toMatchObject({ code: 'vm-residue' }) + expect(oracleCalled).toBe(false) +}) + +test.each([ + ['missing decoder', undefined, 'missing-decoder'], + [ + 'malformed result', + () => ({ output: 'window.TEST_OUTPUT = 1;' }), + 'malformed-recovery', + ], + ['undefined recovery', () => undefined, 'undefined-recovery'], + ['non-string output', () => 42, 'non-string-output'], + ['malformed JavaScript output', () => 'var =', 'parse-output'], +])('detects %s before writing an attempt', async (_label, decoder, code) => { + await expect( + runAttempt(focusedCell, { decoder, name: `negative-${code}` }), + ).rejects.toMatchObject({ code }) +}) + +test('detects an altered decline artifact instead of accepting it', async () => { + const attempt = await runAttempt(rawCell, { + decoder: () => null, + name: 'altered-decline', + }) + fs.writeFileSync(attempt.outputPath, 'altered decline', 'utf8') + expect(() => checkAttempt(attempt)).toThrowError( + expect.objectContaining({ code: 'decline-output-mismatch' }), + ) +}) + +test('ordinary JavaScript near-miss declines and is not classified as VM residue', async () => { + const ordinarySource = 'const answer = 42;\nwindow.TEST_OUTPUT = answer;\n' + const attempt = await runAttempt(rawCell, { + decoder: () => null, + expectedStatus: 'declined', + input: ordinarySource, + name: 'ordinary-javascript-near-miss', + }) + const ast = parseSuccessfulOutput(ordinarySource) + expect(checkVmResidue(ast).ok).toBe(true) + expect(attempt.output).toBe(ordinarySource) +}) + +test('the debug-comment check uses a synthetic comment and catches dependence', () => { + expect(() => + assertDebugCommentIndependent(() => null, rawCell.encodedSource), + ).not.toThrow() + expect(() => + assertDebugCommentIndependent( + (source) => + source.includes('__A0_SYNTHETIC_DEBUG_COMMENT__') ? 'changed' : null, + rawCell.encodedSource, + ), + ).toThrowError(expect.objectContaining({ code: 'debug-comment-dependence' })) +}) + +test('the derived-state detector catches a detached cached reference', () => { + const ast = parse('var value = 1; function read() { return value; }') + traverse(ast, { + Program(path) { + path.scope.crawl() + }, + }) + traverse(ast, { + FunctionDeclaration(path) { + path.node.body.body = [] + }, + }) + expect(() => assertDerivedStateConsistency(ast)).toThrowError( + expect.objectContaining({ code: 'derived-state' }), + ) +}) + +test('the bounded oracle seam aborts a finite delayed callback without target execution', async () => { + let aborted = false + const outcome = await runBoundedBehavioralOracle( + (_request, signal) => + new Promise((resolve) => { + const timer = setTimeout(() => resolve(true), 50) + signal.addEventListener( + 'abort', + () => { + aborted = true + clearTimeout(timer) + resolve(false) + }, + { once: true }, + ) + }), + {}, + { timeoutMs: 5 }, + ) + expect(outcome).toMatchObject({ status: 'timed-out', timedOut: true }) + expect(aborted).toBe(true) +}) + +test('the residue detector has a positive control and a clean control', () => { + const clean = parseSuccessfulOutput('window.TEST_OUTPUT = 1;') + const residual = parseSuccessfulOutput( + 'var BYTECODE = [1]; var CONSTANTS = [];', + ) + expect(findVmResidue(clean)).toEqual({ runtime: [], vm: [], wordcode: [] }) + expect(findVmResidue(residual).wordcode).toEqual(['BYTECODE', 'CONSTANTS']) +}) diff --git a/test/vm/jsconfuser-vm/integration/integration.test.js b/test/vm/jsconfuser-vm/integration/integration.test.js new file mode 100644 index 00000000..b1b74fa0 --- /dev/null +++ b/test/vm/jsconfuser-vm/integration/integration.test.js @@ -0,0 +1,62 @@ +import fs from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { spawnSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseStandalone } from '../../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { assertNoVmResidue, parseSuccessfulOutput } from './harness.js' + +const packageRoot = fileURLToPath(new URL('../../../../', import.meta.url)) +const fixtureRoot = new URL( + '../fixtures/corpus/raw/f-branching/', + import.meta.url, +) + +test('registered CLI preserves a structured branch and matches direct diagnosis', () => { + const inputPath = fileURLToPath(new URL('encoded.js', fixtureRoot)) + const input = fs.readFileSync(inputPath, 'utf8') + const expected = JSON.parse( + fs.readFileSync(new URL('oracle.json', fixtureRoot), 'utf8'), + ).expected + const directory = fs.mkdtempSync(path.join(tmpdir(), 'jsconfuser-vm-branch-')) + try { + const outputPath = path.join(directory, 'decoded.js') + const resultPath = path.join(directory, 'result.json') + const completed = spawnSync( + process.execPath, + [ + 'src/main.js', + '-t', + 'jsconfuser-vm', + '-i', + inputPath, + '-o', + outputPath, + '--result', + resultPath, + ], + { cwd: packageRoot, encoding: 'utf8', timeout: 30000 }, + ) + expect(completed.error).toBeUndefined() + expect(completed.status).toBe(0) + + const output = fs.readFileSync(outputPath, 'utf8') + const record = JSON.parse(fs.readFileSync(resultPath, 'utf8')) + const direct = diagnoseStandalone(input) + expect(direct.ok).toBe(true) + expect(record.result).toEqual(direct.result) + expect(record.output).toBe(output) + expect(output).toContain('if (') + expect(output).not.toContain('__recovered_dispatch') + expect(assertNoVmResidue(parseSuccessfulOutput(output)).ok).toBe(true) + + const context = { TEST_OUTPUT: null } + context.window = context + runInNewContext(output, context, { timeout: 2000 }) + expect(context.TEST_OUTPUT).toEqual(expected) + } finally { + fs.rmSync(directory, { recursive: true, force: true }) + } +}) diff --git a/test/vm/jsconfuser-vm/integration/plugin.test.js b/test/vm/jsconfuser-vm/integration/plugin.test.js new file mode 100644 index 00000000..0ac1c368 --- /dev/null +++ b/test/vm/jsconfuser-vm/integration/plugin.test.js @@ -0,0 +1,137 @@ +import crypto from 'node:crypto' +import { beforeEach, expect, test, vi } from 'vitest' + +const { diagnoseStandaloneSpy } = vi.hoisted(() => ({ + diagnoseStandaloneSpy: vi.fn(), +})) + +vi.mock('../../../../src/vm/jsconfuser-vm/decode-standalone.js', async () => { + const actual = await vi.importActual( + '../../../../src/vm/jsconfuser-vm/decode-standalone.js', + ) + diagnoseStandaloneSpy.mockImplementation(actual.diagnoseStandalone) + return { ...actual, diagnoseStandalone: diagnoseStandaloneSpy } +}) + +import decodeJsconfuserVm from '../../../../src/plugin/jsconfuser-vm.js' +import normalizeVmInput from '../../../../src/visitor/jsconfuser-vm/normalize-source.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + loadVmCell, + parseSuccessfulOutput, +} from './harness.js' + +const rawCell = loadVmCell('f-literals-order') +const focusedCell = loadVmCell('focused-call-method-spread', { + kind: 'focused', +}) + +function sha256(source) { + return crypto.createHash('sha256').update(source).digest('hex') +} + +function expectSingleDiagnosis(input) { + expect(diagnoseStandaloneSpy).toHaveBeenCalledTimes(1) + expect(diagnoseStandaloneSpy).toHaveBeenCalledWith(normalizeVmInput(input)) +} + +beforeEach(() => { + diagnoseStandaloneSpy.mockClear() +}) + +test.each([ + ['reference', rawCell], + ['focused', focusedCell], +])( + 'returns fresh emitted JavaScript and standalone result for the %s fixture', + (_label, cell) => { + const adapterResult = decodeJsconfuserVm(cell.encodedSource) + expectSingleDiagnosis(cell.encodedSource) + + expect(adapterResult).toMatchObject({ + schemaVersion: 'jsconfuser-vm-adapter.v1', + ok: true, + status: 'success', + input: cell.encodedSource, + diagnostic: null, + }) + expect(adapterResult.result).not.toHaveProperty('packet') + expect(adapterResult.result).toMatchObject({ + schemaVersion: 'jsconfuser-vm-standalone.v2', + parseOnly: true, + targetExecution: false, + vmExecuted: false, + emittedJavaScript: true, + finalJavaScriptSemantics: false, + proof: { + allPredecessorsReconstructed: true, + noTargetExecution: true, + noVmRuntimeEmission: true, + freshJavaScriptEmission: true, + }, + }) + expect(adapterResult.output).toBe(adapterResult.result.output) + expect(adapterResult.output).not.toBe(cell.encodedSource) + expect(Object.isFrozen(adapterResult)).toBe(true) + + const ast = parseSuccessfulOutput(adapterResult.output) + expect(assertNoVmResidue(ast).ok).toBe(true) + expect(assertDerivedStateConsistency(ast, adapterResult.output).ok).toBe( + true, + ) + }, +) + +test.each([ + [ + 'ordinary JavaScript near-miss', + 'const answer = 42;\nwindow.TEST_OUTPUT = answer;\n', + 'container-declined', + ], + [ + 'malformed truncated fixture', + rawCell.encodedSource.slice(0, -20), + 'container-declined', + ], +])( + 'returns exact input and stable diagnostic for %s', + (_label, input, diagnosticCode) => { + const adapterResult = decodeJsconfuserVm(input) + expectSingleDiagnosis(input) + + expect(adapterResult).toMatchObject({ + schemaVersion: 'jsconfuser-vm-adapter.v1', + ok: false, + status: 'declined', + input, + output: input, + result: null, + diagnostic: { + code: diagnosticCode, + message: expect.any(String), + }, + }) + expect(adapterResult.output).toBe(input) + expect(Buffer.byteLength(adapterResult.output)).toBe( + Buffer.byteLength(input), + ) + expect(sha256(adapterResult.output)).toBe(sha256(input)) + expect(Object.isFrozen(adapterResult)).toBe(true) + }, +) + +test('does not depend on a leading debug comment', () => { + const debugInput = `// __S4_A_SYNTHETIC_DEBUG_COMMENT__\n${focusedCell.encodedSource}` + const plain = decodeJsconfuserVm(focusedCell.encodedSource) + diagnoseStandaloneSpy.mockClear() + const withDebug = decodeJsconfuserVm(debugInput) + expectSingleDiagnosis(debugInput) + + expect(withDebug.status).toBe('success') + expect(withDebug.output).toBe(plain.output) + expect(withDebug.result).toMatchObject({ + targetExecution: false, + vmExecuted: false, + }) +}) diff --git a/test/vm/jsconfuser-vm/integration/sequential.test.js b/test/vm/jsconfuser-vm/integration/sequential.test.js new file mode 100644 index 00000000..9666959c --- /dev/null +++ b/test/vm/jsconfuser-vm/integration/sequential.test.js @@ -0,0 +1,219 @@ +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' +import vm from 'node:vm' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { expect, test } from 'vitest' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' + +import { decodeJsconfuserVmSequential } from '../../../../src/plugin/jsconfuser-vm-sequential.js' + +const testDirectory = path.dirname(fileURLToPath(import.meta.url)) +const packageRoot = path.resolve(testDirectory, '../../../..') +const outerInputPath = path.join( + packageRoot, + 'test/jsconfuser/control-flow-flattening-scope-capture.js', +) +const vmInputPath = path.join( + packageRoot, + 'test/vm/jsconfuser-vm/fixtures/corpus/raw/f-literals-order/encoded.js', +) + +test('outer decoding cannot publish a partial VM result', () => { + const input = fs.readFileSync(outerInputPath, 'utf8') + const record = decodeJsconfuserVmSequential(input) + + expect(record).toMatchObject({ + schemaVersion: 'jsconfuser-vm-sequential.v1', + ok: false, + input, + output: input, + result: null, + failedStage: 'vm', + diagnostic: { stage: 'vm', code: 'container-declined' }, + stages: { + outer: { status: 'success', ok: true, output: expect.any(String) }, + vm: { status: 'declined', ok: false }, + }, + }) + expect(record.stages.outer.output).not.toBe(input) + expect(Object.isFrozen(record)).toBe(true) +}) + +test('ordinary source remains unchanged when no VM container exists', () => { + const input = 'const answer = 42;\nwindow.TEST_OUTPUT = answer;\n' + const record = decodeJsconfuserVmSequential(input) + expect(record.output).toBe(input) + expect(record.diagnostic).toMatchObject({ + stage: 'vm', + code: 'container-declined', + }) +}) + +test('visible numeric VM input completes through both decoders', () => { + const input = fs.readFileSync(vmInputPath, 'utf8') + const record = decodeJsconfuserVmSequential(input) + + expect(record).toMatchObject({ + schemaVersion: 'jsconfuser-vm-sequential.v1', + ok: true, + status: 'success', + input, + diagnostic: null, + failedStage: null, + result: { schemaVersion: 'jsconfuser-vm-standalone.v2' }, + stages: { + outer: { status: 'success', ok: true }, + vm: { status: 'success', ok: true }, + }, + }) + expect(record.output).toBe(record.result.output) + expect(record.output).not.toBe(input) + expect(Object.isFrozen(record.result)).toBe(true) +}) + +test('invalid source declines at the outer decoder', () => { + const input = 'function (' + const record = decodeJsconfuserVmSequential(input) + expect(record).toMatchObject({ + ok: false, + output: input, + failedStage: 'outer', + diagnostic: { stage: 'outer', code: 'outer-declined' }, + stages: { vm: { status: 'pending' } }, + }) +}) + +const fixtureFile = (folder, name) => + fs.readFileSync( + path.join(packageRoot, 'test/vm/jsconfuser-vm/fixtures', folder, name), + 'utf8', + ) +const layerPair = (name) => fixtureFile('layer-pair-arithmetic', `${name}.js`) + +function switchCount(source) { + let count = 0 + traverse(parse(source), { + SwitchStatement() { + count++ + }, + }) + return count +} + +test('the layer-pair VM layer decodes on its own', () => { + const input = layerPair('vm-only') + const record = decodeJsconfuserVmSequential(input) + expect(record.ok).toBe(true) + expect(record.output).not.toBe(input) +}) + +// layer-pair-arithmetic keeps canonical opcodes; outer-layers randomizes them under one outer +// option each, so the handlers must be named through the outer layer's re-spelling. +test.each([ + ['layer-pair-arithmetic', 'outer-complete.js'], + ['layer-pair-arithmetic', 'outer-without-string-concealing.js'], + ['outer-layers', 'compact.encoded.js'], + ['outer-layers', 'object-extraction.encoded.js'], + ['outer-layers', 'moved-declarations.encoded.js'], + ['outer-layers', 'minify.encoded.js'], + ['outer-layers', 'variable-masking.encoded.js'], +])( + '%s %s: VM preparation recovers the wrapped program without runtime residue', + (folder, name) => { + const input = fixtureFile(folder, name) + const record = decodeJsconfuserVmSequential(input) + expect(record).toMatchObject({ + ok: true, + failedStage: null, + stages: { outer: { ok: true }, vm: { ok: true } }, + }) + expect(record.output).not.toBe(input) + expect(switchCount(record.stages.outer.output)).toBe(1) + expect(switchCount(record.output)).toBe(0) + // The VM payload is the word stream and the constant pool: arrays of literals only. Recovered + // calls pass their arguments as arrays of registers, which are program code. + const literal = (node) => + [ + 'NumericLiteral', + 'StringLiteral', + 'BooleanLiteral', + 'NullLiteral', + ].includes(node?.type) || + (node?.type === 'UnaryExpression' && + node.operator === '-' && + node.argument.type === 'NumericLiteral') + const residue = (source) => { + let payloads = 0 + let installations = 0 + traverse(parse(source), { + ArrayExpression(path) { + if (path.node.elements.length && path.node.elements.every(literal)) + payloads++ + }, + AssignmentExpression(path) { + if ( + path.node.left.type === 'MemberExpression' && + path.node.right.type === 'FunctionExpression' + ) + installations++ + }, + }) + return { payloads, installations } + } + // Control: the outer stage's output still holds the payload the VM stage removes. + expect(residue(record.stages.outer.output).payloads).toBeGreaterThan(0) + const { payloads, installations } = residue(record.output) + expect(payloads).toBe(0) + expect(installations).toBe(0) + const observe = (source) => { + const context = { Buffer, performance: { now: () => 0 } } + context.window = context + vm.runInNewContext(source, context, { timeout: 1000 }) + return context.TEST_OUTPUT + } + const source = fixtureFile(folder, 'source.js') + expect(observe(record.output)).toBe(observe(source)) + expect(observe(input)).toBe(observe(source)) + }, +) + +test('sequential CLI writes the original source and diagnosis on VM decline', () => { + const directory = fs.mkdtempSync( + path.join(tmpdir(), 'jsconfuser-vm-sequential-'), + ) + const inputPath = path.join(directory, 'input.js') + const outputPath = path.join(directory, 'output.js') + const resultPath = path.join(directory, 'result.json') + const input = fs.readFileSync(outerInputPath, 'utf8') + fs.writeFileSync(inputPath, input) + + const completed = spawnSync( + process.execPath, + [ + 'src/main.js', + '-t', + 'jsconfuser-vm-sequential', + '-i', + inputPath, + '-o', + outputPath, + '--result', + resultPath, + ], + { cwd: packageRoot, encoding: 'utf8' }, + ) + + expect(completed.status).toBe(0) + expect(completed.stdout).toContain('Status: declined') + expect(completed.stderr).toContain('Diagnostic: container-declined') + expect(fs.readFileSync(outputPath, 'utf8')).toBe(input) + expect(JSON.parse(fs.readFileSync(resultPath, 'utf8'))).toMatchObject({ + schemaVersion: 'jsconfuser-vm-sequential.v1', + status: 'declined', + output: input, + diagnostic: { stage: 'vm', code: 'container-declined' }, + }) +}) diff --git a/test/vm/jsconfuser-vm/minified-class-admission.behavior.test.js b/test/vm/jsconfuser-vm/minified-class-admission.behavior.test.js new file mode 100644 index 00000000..ce8a05c3 --- /dev/null +++ b/test/vm/jsconfuser-vm/minified-class-admission.behavior.test.js @@ -0,0 +1,39 @@ +// Native boundary: this test requires separately retained local fixture data. +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { + currentExceptionRecovery, + observeException, +} from './minified-class-behavior-support.js' +const root = new URL('fixtures/minified-class-admission/', import.meta.url) +const source = fs.readFileSync( + new URL('exception-routes.source.js', root), + 'utf8', +) +const expected = JSON.parse( + fs.readFileSync(new URL('exception-routes.expected.json', root), 'utf8'), +) +// Required local data fail before native observation when not supplied. +const provenance = JSON.parse( + fs.readFileSync(new URL('native-qualification.json', root), 'utf8'), +) +expect(provenance.cells).toHaveLength(12) +for (const profile of [ + 'baseline', + 'minify', + 'classObfuscation', + 'minifyClassObfuscation', +]) + for (const seed of [172, 173, 174]) + test(`exceptionRoutes/${profile}/${seed}: current output native closure`, () => { + const cell = provenance.cells.find( + (cell) => cell.profile === profile && cell.seed === seed, + ) + expect(cell).toBeDefined() + expect(cell.encodedFile).toBe(`${profile}-${seed}.encoded.js`) + const encoded = fs.readFileSync(new URL(cell.encodedFile, root), 'utf8') + const output = currentExceptionRecovery(encoded) + expect(observeException(source)).toEqual(expected) + expect(observeException(encoded)).toEqual(expected) + expect(observeException(output)).toEqual(expected) + }) diff --git a/test/vm/jsconfuser-vm/minified-class-admission.test.js b/test/vm/jsconfuser-vm/minified-class-admission.test.js new file mode 100644 index 00000000..76b3a434 --- /dev/null +++ b/test/vm/jsconfuser-vm/minified-class-admission.test.js @@ -0,0 +1,136 @@ +import fs from 'node:fs' +import { parse } from '@babel/parser' +import generate from '@babel/generator' +import traverse from '@babel/traverse' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { diagnoseReferences } from '../../../src/vm/jsconfuser-vm/validate-references.js' +import { decodeJsconfuserVm } from '../../../src/plugin/jsconfuser-vm.js' +import { expectConsistentState } from '../../helper.js' +import { + parseSuccessfulOutput, + assertDerivedStateConsistency, + assertNoVmResidue, +} from './integration/harness.js' + +const sourceAt = (kind, seed) => + fs.readFileSync( + new URL( + `fixtures/${kind === 'arithmetic' ? 'minified-class-boot' : `minified-class-runtime/${kind}`}/encoded-${seed}.js`, + import.meta.url, + ), + 'utf8', + ) +const select = (ast, type, predicate) => { + let selected + traverse(ast, { + [type](path) { + if (!selected && predicate(path)) selected = path + }, + }) + expect(selected, `${type} selector reaches source`).toBeDefined() + return selected +} +function damaged(source, change) { + const ast = parse(source, { sourceType: 'script' }) + change(ast) + return generate(ast).code +} +function expectPublicDecline(source, code) { + expect(decodeJsconfuserVm(source)).toMatchObject({ + ok: false, + result: null, + output: source, + diagnostic: { code }, + }) +} + +for (const kind of ['arithmetic', 'exceptions', 'nested-strings']) + for (const seed of [169, 170, 171]) { + test(`${kind}/${seed}: A, B and public source with consistent state`, () => { + const source = sourceAt(kind, seed), + ast = parse(source, { sourceType: 'script' }) + traverse(ast, {}) + expectConsistentState(ast, source, { sourceType: 'script' }) + const before = JSON.stringify(ast), + a = diagnoseContainer(ast) + expect(a.ok, JSON.stringify(a.diagnostic)).toBe(true) + expect(JSON.stringify(ast)).toBe(before) + expectConsistentState(ast, source, { sourceType: 'script' }) + const b = diagnoseWordcode(a.result) + expect(b.ok).toBe(true) + expect(b.result.consumedWords).toBe(a.result.roles.words.values.length) + const result = decodeJsconfuserVm(source) + expect(result.ok, JSON.stringify(result.diagnostic)).toBe(true) + expect(result.output).not.toBe(source) + expect(result.output).toBe(result.result.output) + expect(result.result.proof).toMatchObject({ + noTargetExecution: true, + freshJavaScriptEmission: true, + freshSimplificationState: true, + finalParseValidated: true, + finalResidueValidated: true, + }) + const outputAst = parseSuccessfulOutput(result.output) + expect(assertNoVmResidue(outputAst).ok).toBe(true) + expect(assertDerivedStateConsistency(outputAst, result.output).ok).toBe( + true, + ) + expect(Object.isFrozen(a.result.roles.pool.values)).toBe(true) + expect(Object.isFrozen(result)).toBe(true) + }) + } + +test('payload source damage passes A before exact B/C refusals', () => { + const source = sourceAt('exceptions', 169), + a = diagnoseContainer(source).result, + words = a.roles.words.values + const changeWords = (change) => + damaged(source, (ast) => { + const array = select( + ast, + 'ArrayExpression', + (path) => + path.node.elements.length === words.length && + path.node.elements.every((item, i) => item.value === words[i]), + ) + change(array.node.elements) + }) + const unknown = changeWords((elements) => { + elements[0].value = 0xffffffff + }) + const unknownA = diagnoseContainer(unknown) + expect(unknownA.ok).toBe(true) + expect(diagnoseWordcode(unknownA.result).diagnostic).toEqual({ + code: 'unknown-opcode', + message: 'Unknown opcode 4294967295 at pc 0', + }) + expectPublicDecline(unknown, 'wordcode-declined') + const truncated = changeWords((elements) => { + elements.pop() + }) + expect(diagnoseContainer(truncated).ok).toBe(true) + expect( + diagnoseWordcode(diagnoseContainer(truncated).result).diagnostic, + ).toEqual({ + code: 'truncated-instruction', + message: 'RETURN at 177 is missing a operand', + }) + expectPublicDecline(truncated, 'wordcode-declined') + const b = diagnoseWordcode(a).result, + branch = b.instructions.find((item) => item.name === 'JUMP_IF_FALSE') + expect(branch).toBeDefined() + const badTarget = changeWords((elements) => { + elements[branch.pc + 2].value = branch.pc + 1 + }) + const cA = diagnoseContainer(badTarget), + cB = diagnoseWordcode(cA.result) + expect(cA.ok).toBe(true) + expect(cB.ok).toBe(true) + expect(diagnoseReferences(cA.result, cB.result).diagnostic).toEqual({ + code: 'invalid-label-reference', + message: 'JUMP_IF_FALSE@87 targets non-boundary pc 88', + }) + expectPublicDecline(badTarget, 'references-declined') +}) diff --git a/test/vm/jsconfuser-vm/minified-class-behavior-support.js b/test/vm/jsconfuser-vm/minified-class-behavior-support.js new file mode 100644 index 00000000..769078f6 --- /dev/null +++ b/test/vm/jsconfuser-vm/minified-class-behavior-support.js @@ -0,0 +1,59 @@ +import { expect } from 'vitest' +import { runInNewContext } from 'node:vm' +import { performance } from 'node:perf_hooks' +import { diagnoseStandaloneInput } from '../../../src/vm/jsconfuser-vm/diagnose-standalone.js' +import { decodeJsconfuserVm } from '../../../src/plugin/jsconfuser-vm.js' +import { + parseSuccessfulOutput, + assertNoVmResidue, + assertDerivedStateConsistency, +} from './integration/harness.js' + +export function currentExceptionRecovery(encoded) { + const analysis = diagnoseStandaloneInput(encoded) + expect(analysis.ok, JSON.stringify(analysis.diagnostic)).toBe(true) + const instructions = analysis.model.wordcode.instructions + for (const name of [ + 'CALL_METHOD', + 'GET_PROP', + 'THROW', + 'TRY_SETUP', + 'FINALLY_SETUP', + 'MAKE_CLOSURE', + 'RETURN', + 'LOAD_UPVALUE', + 'STORE_UPVALUE', + ]) + expect( + instructions.some((item) => item.name === name), + `mandatory ${name} payload route`, + ).toBe(true) + const at = (pc) => instructions.find((item) => item.pc === pc) + for (const setup of instructions.filter((item) => item.name === 'TRY_SETUP')) + expect(at(setup.operands[0].pc)).toBeDefined() + for (const setup of instructions.filter( + (item) => item.name === 'FINALLY_SETUP', + )) { + expect(at(setup.operands[0].pc)).toBeDefined() + expect(at(setup.operands[3].pc)?.name).toBe('THROW') + } + expect( + analysis.model.functions.functions.some( + (fn) => fn.captureCount > 0 && fn.descriptor.captures.length > 0, + ), + ).toBe(true) + const current = decodeJsconfuserVm(encoded) + expect(current.ok, JSON.stringify(current.diagnostic)).toBe(true) + expect(current.output).not.toBe(encoded) + const ast = parseSuccessfulOutput(current.output) + expect(assertNoVmResidue(ast).ok).toBe(true) + expect(assertDerivedStateConsistency(ast, current.output).ok).toBe(true) + return current.output +} +export function observeException(source) { + const context = { TEST_OUTPUT: undefined, performance, atob, btoa } + context.window = context + context.self = context + runInNewContext(source, context, { timeout: 10000 }) + return context.TEST_OUTPUT +} diff --git a/test/vm/jsconfuser-vm/minified-class-frontend.test.js b/test/vm/jsconfuser-vm/minified-class-frontend.test.js new file mode 100644 index 00000000..37b085fb --- /dev/null +++ b/test/vm/jsconfuser-vm/minified-class-frontend.test.js @@ -0,0 +1,51 @@ +import fs from 'node:fs' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import { expect, test } from 'vitest' +import { expectConsistentState } from '../../helper.js' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { decodeJsconfuserVm } from '../../../src/plugin/jsconfuser-vm.js' + +const directory = new URL('fixtures/minified-class-frontend/', import.meta.url) +const provenance = JSON.parse( + fs.readFileSync(new URL('provenance.json', directory), 'utf8'), +) +const read = (seed, kind = 'arithmetic') => + fs.readFileSync( + new URL(`${kind}--minifyClassObfuscation--${seed}.js`, directory), + 'utf8', + ) +test('local frozen parser fixtures retain config/seed/pin records', () => { + expect(provenance.encoderPin).toBe('20c5b96bf57337c568579352758d7685358650de') + for (const record of provenance.records) { + expect(record.options.minify).toBe( + ['minify', 'minifyClassObfuscation'].includes(record.profile), + ) + expect(record.options.classObfuscation).toBe( + ['classObfuscation', 'minifyClassObfuscation'].includes(record.profile), + ) + } +}) +for (const seed of [172, 173, 174]) { + test(`${seed}: A locates the payload`, () => { + expect(diagnoseContainer(read(seed)).ok).toBe(true) + }) + test(`${seed}: mandatory exception input reaches structured shared N output`, () => { + const source = read(seed, 'exceptionRoutes') + expect(diagnoseContainer(source).ok).toBe(true) + const result = decodeJsconfuserVm(source) + expect(result.ok, JSON.stringify(result.diagnostic)).toBe(true) + expect(result.output).not.toBe(source) + expect(result.output).toContain('catch (__recovered_error)') + expect(result.output).toContain('} finally {') + expect(result.output).not.toMatch(/__recovered_(?:dispatch|pc|handlers)/) + expect(result.result.proof).toMatchObject({ + freshJavaScriptEmission: true, + finalParseValidated: true, + finalResidueValidated: true, + }) + const outputAst = parse(result.output) + traverse(outputAst, {}) + expectConsistentState(outputAst, result.output) + }) +} diff --git a/test/vm/jsconfuser-vm/randomized-class.test.js b/test/vm/jsconfuser-vm/randomized-class.test.js new file mode 100644 index 00000000..7aabce31 --- /dev/null +++ b/test/vm/jsconfuser-vm/randomized-class.test.js @@ -0,0 +1,161 @@ +import fs from 'node:fs' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import generate from '@babel/generator' +import { test, expect } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { decodeJsconfuserVm } from '../../../src/plugin/jsconfuser-vm.js' +import { expectConsistentState } from '../../helper.js' +import { + parseSuccessfulOutput, + assertNoVmResidue, + assertDerivedStateConsistency, +} from './integration/harness.js' + +// Canonical word meaning: readWordcode's instruction names and operands, the spread sentinel by +// kind only. +function meaningOf(container) { + return diagnoseWordcode(container).result.instructions.map( + ({ name, operands }) => ({ + name, + operands: operands.map((operand) => + operand.kind === 'spread-sentinel' ? { kind: operand.kind } : operand, + ), + }), + ) +} + +const sourceAt = (name) => + fs.readFileSync( + new URL(`fixtures/randomized-class/${name}.js`, import.meta.url), + 'utf8', + ) +for (const kind of ['arithmetic', 'spread', 'exceptionRoutes']) + for (const seed of [181, 182, 183]) + test(`${kind}/${seed}: canonical meaning and actual recovery`, () => { + const source = sourceAt(`${kind}-${seed}`), + ast = parse(source) + traverse(ast, {}) + const before = JSON.stringify(ast) + const A = diagnoseContainer(ast), + baseline = diagnoseContainer(sourceAt(`${kind}.baseline`)) + expect(A.ok, JSON.stringify(A.diagnostic)).toBe(true) + expect(JSON.stringify(ast)).toBe(before) + expectConsistentState(ast, source, { sourceType: 'script' }) + expect(meaningOf(A.result)).toEqual(meaningOf(baseline.result)) + expect(A.result.roles.pool.values).toEqual( + baseline.result.roles.pool.values, + ) + const B = diagnoseWordcode(A.result) + expect(B.ok).toBe(true) + expect(B.result.consumedWords).toBe(A.result.roles.words.values.length) + const result = decodeJsconfuserVm(source) + expect(result.ok, JSON.stringify(result.diagnostic)).toBe(true) + expect(result.output).not.toBe(source) + expect(result.result.finalJavaScriptSemantics).toBe(false) + expect(result.result.proof).toMatchObject({ + freshJavaScriptEmission: true, + freshSimplificationState: true, + finalParseValidated: true, + finalResidueValidated: true, + }) + const output = parseSuccessfulOutput(result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, result.output).ok).toBe(true) + }) + +function change(source, recipe) { + const ast = parse(source) + let dispatch + traverse(ast, { + SwitchStatement(path) { + dispatch = path + }, + }) + recipe({ ast, dispatch }) + return generate(ast).code +} +for (const seed of [181, 182, 183]) { + const source = sourceAt(`arithmetic-${seed}`) + test(`${seed}: case reordering and complete consistent mapping are structural`, () => { + const reordered = change(source, ({ dispatch }) => { + dispatch.node.cases.reverse() + }) + expect(diagnoseContainer(reordered).ok).toBe(true) + const alpha = change(source, ({ ast }) => { + traverse(ast, { + Program(path) { + for (const name of Object.keys(path.scope.bindings)) + path.scope.rename(name, `renamed_${name}`) + }, + }) + }) + expect(diagnoseContainer(alpha).ok).toBe(true) + }) +} + +// Consistent full remapping: every case label, every instruction-boundary +// opcode word and the disabled-timing `op = JUMP` literal (the one numeric write of a label to +// the dispatch selector) move together. +function remapped(source) { + const A = diagnoseContainer(source) + expect(A.ok).toBe(true) + const B = diagnoseWordcode(A.result) + expect(B.ok).toBe(true) + const words = A.result.roles.words.values + return change(source, ({ ast, dispatch }) => { + const remap = new Map( + dispatch.node.cases + .filter((item) => item.test) + .map(({ test }) => [test.value, (test.value + 1) % 65536]), + ) + const opcode = dispatch.scope.getBinding(dispatch.node.discriminant.name) + let jumpUses = 0 + traverse(ast, { + AssignmentExpression(path) { + const { left, right } = path.node + if ( + left.type === 'Identifier' && + path.scope.getBinding(left.name) === opcode && + right.type === 'NumericLiteral' && + remap.has(right.value) + ) { + jumpUses++ + right.value = remap.get(right.value) + } + }, + }) + expect(jumpUses).toBe(1) + for (const item of dispatch.node.cases) + if (item.test) item.test.value = remap.get(item.test.value) + const arrays = [] + traverse(ast, { + ArrayExpression(path) { + const { elements } = path.node + if ( + elements.length === words.length && + elements.every((element) => element?.type === 'NumericLiteral') + ) + arrays.push(elements) + }, + }) + expect(arrays).toHaveLength(1) + for (const { pc } of B.result.instructions) { + expect(remap.has(arrays[0][pc].value)).toBe(true) + arrays[0][pc].value = remap.get(arrays[0][pc].value) + } + }) +} +for (const seed of [181, 182, 183]) + test(`${seed}: consistent full remapping admits identical canonical meaning`, () => { + const source = sourceAt(`arithmetic-${seed}`) + const original = meaningOf(diagnoseContainer(source).result) + const shifted = remapped(source) + const A = diagnoseContainer(shifted) + expect(A.ok, JSON.stringify(A.diagnostic)).toBe(true) + expect(meaningOf(A.result)).toEqual(original) + const result = decodeJsconfuserVm(shifted) + expect(result.ok, JSON.stringify(result.diagnostic)).toBe(true) + expect(result.output).not.toBe(shifted) + }) diff --git a/test/vm/jsconfuser-vm/randomized-minified-class.test.js b/test/vm/jsconfuser-vm/randomized-minified-class.test.js new file mode 100644 index 00000000..7df13a41 --- /dev/null +++ b/test/vm/jsconfuser-vm/randomized-minified-class.test.js @@ -0,0 +1,133 @@ +import fs from 'node:fs' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import generate from '@babel/generator' +import { test, expect } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { decodeJsconfuserVm } from '../../../src/plugin/jsconfuser-vm.js' +import { expectConsistentState } from '../../helper.js' +import { + assertNoVmResidue, + assertDerivedStateConsistency, + parseSuccessfulOutput, +} from './integration/harness.js' + +const sourceAt = (name) => + fs.readFileSync( + new URL(`fixtures/randomized-class/${name}.js`, import.meta.url), + 'utf8', + ) +// Canonical word meaning: readWordcode's instruction names and operands, the spread sentinel by +// kind only. +function meaningOf(container) { + return diagnoseWordcode(container).result.instructions.map( + ({ name, operands }) => ({ + name, + operands: operands.map((operand) => + operand.kind === 'spread-sentinel' ? { kind: operand.kind } : operand, + ), + }), + ) +} +for (const kind of ['arithmetic', 'spread', 'exceptionRoutes']) + for (const seed of [191, 192, 193]) + test(`${kind}/${seed}: randomized minified canonical meaning and actual recovery`, () => { + const source = sourceAt(`${kind}-minified-${seed}`), + ast = parse(source) + traverse(ast, {}) + const before = JSON.stringify(ast) + const A = diagnoseContainer(ast), + baseline = diagnoseContainer(sourceAt(`${kind}.baseline`)) + expect(A.ok, JSON.stringify(A.diagnostic)).toBe(true) + expect(meaningOf(A.result)).toEqual(meaningOf(baseline.result)) + expect(A.result.roles.pool.values).toEqual( + baseline.result.roles.pool.values, + ) + expect(diagnoseWordcode(A.result).result.consumedWords).toBe( + A.result.roles.words.values.length, + ) + expect(JSON.stringify(ast)).toBe(before) + expectConsistentState(ast, source, { sourceType: 'script' }) + const result = decodeJsconfuserVm(source) + expect(result.ok, JSON.stringify(result.diagnostic)).toBe(true) + expect(result.output).not.toBe(source) + expect(result.result.finalJavaScriptSemantics).toBe(false) + expect(result.result.proof).toMatchObject({ + freshJavaScriptEmission: true, + freshSimplificationState: true, + finalParseValidated: true, + finalResidueValidated: true, + }) + const output = parseSuccessfulOutput(result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect(assertDerivedStateConsistency(output, result.output).ok).toBe(true) + }) +function damaged(seed, recipe) { + const ast = parse(sourceAt(`arithmetic-minified-${seed}`)) + let dispatch + traverse(ast, { + SwitchStatement(path) { + dispatch = path + }, + }) + recipe({ ast, dispatch }) + return generate(ast).code +} +for (const seed of [191, 192, 193]) { + test(`${seed}: alpha bindings and case order admit`, () => { + const source = damaged(seed, ({ ast, dispatch }) => { + dispatch.node.cases.reverse() + traverse(ast, { + Program(p) { + for (const name of Object.keys(p.scope.bindings)) + p.scope.rename(name, `renamed_${name}`) + }, + }) + }) + expect(diagnoseContainer(source).ok).toBe(true) + }) +} + +// Consistent full remapping: every case label and every instruction-boundary +// opcode word move together. Closure folds the disabled-timing branch, so the +// minified source has no other owned opcode use to remap. +for (const seed of [191, 192, 193]) + test(`${seed}: consistent full remapping admits identical canonical meaning`, () => { + const original = diagnoseContainer(sourceAt(`arithmetic-minified-${seed}`)) + expect(original.ok).toBe(true) + const B = diagnoseWordcode(original.result) + expect(B.ok).toBe(true) + const words = original.result.roles.words.values + const source = damaged(seed, ({ ast, dispatch }) => { + const remap = new Map( + dispatch.node.cases + .filter((item) => item.test) + .map(({ test }) => [test.value, (test.value + 1) % 65536]), + ) + for (const item of dispatch.node.cases) + if (item.test) item.test.value = remap.get(item.test.value) + const arrays = [] + traverse(ast, { + ArrayExpression(p) { + const { elements } = p.node + if ( + elements.length === words.length && + elements.every((element) => element?.type === 'NumericLiteral') + ) + arrays.push(elements) + }, + }) + expect(arrays).toHaveLength(1) + for (const { pc } of B.result.instructions) { + expect(remap.has(arrays[0][pc].value)).toBe(true) + arrays[0][pc].value = remap.get(arrays[0][pc].value) + } + }) + const A = diagnoseContainer(source) + expect(A.ok, JSON.stringify(A.diagnostic)).toBe(true) + expect(meaningOf(A.result)).toEqual(meaningOf(original.result)) + const result = decodeJsconfuserVm(source) + expect(result.ok, JSON.stringify(result.diagnostic)).toBe(true) + expect(result.output).not.toBe(source) + }) diff --git a/test/vm/jsconfuser-vm/runtime-binding-identities.test.js b/test/vm/jsconfuser-vm/runtime-binding-identities.test.js new file mode 100644 index 00000000..3adcb285 --- /dev/null +++ b/test/vm/jsconfuser-vm/runtime-binding-identities.test.js @@ -0,0 +1,73 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { parse } from '@babel/parser' +import generate from '@babel/generator' +import traverse from '@babel/traverse' +import { expect, test } from 'vitest' +import { makeContext } from '../../../src/vm/jsconfuser-vm/container-role-helpers.js' +import { runtimeSignature } from '../../../src/vm/jsconfuser-vm/runtime-source-signature.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import decodeJsconfuserVm from '../../../src/plugin/jsconfuser-vm.js' + +function renamedBindings(source) { + const ast = parse(source, { sourceType: 'script' }) + const scopes = new Set() + traverse(ast, { + enter(path) { + scopes.add(path.scope) + }, + }) + let index = 0 + for (const scope of scopes) { + for (const name of Object.keys(scope.bindings)) + scope.rename(name, `lexical_binding_${index++}`) + } + return generate(ast).code +} + +function observe(source) { + const window = {} + vm.runInNewContext( + source, + { window, atob, btoa, Buffer, performance: { now: () => 0 } }, + { timeout: 30000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test.each([ + 'encoded-bytecode-arithmetic', + 'concealed-constants-exceptions', + 'randomized-runtime-exceptions', + 'class-handler-alias-stack-exceptions', + 'encoded-class-stack-exceptions', +])('decodes %s after every lexical binding is renamed', (name) => { + const directory = new URL(`fixtures/${name}/`, import.meta.url) + const source = fs.readFileSync(new URL('source.js', directory), 'utf8') + const encoded = fs.readFileSync(new URL('encoded.js', directory), 'utf8') + const renamed = renamedBindings(encoded) + expect(renamed).not.toBe(encoded) + const diagnosis = diagnoseStandalone(renamed) + expect(diagnosis.diagnostic).toBe(null) + expect(diagnosis.ok).toBe(true) + expect(observe(renamed)).toEqual(observe(source)) + expect(observe(diagnosis.result.output)).toEqual(observe(source)) + // The public entry prepares its input first; it must recover the same program. + expect(decodeJsconfuserVm(renamed).output).toBe(diagnosis.result.output) +}) + +test('complete-body signatures preserve lexical binding topology', () => { + const signature = (source) => { + const ast = parse(source) + return runtimeSignature(ast.program.body[0], makeContext(ast)) + } + expect(signature('function one(value) { return value + value }')).toBe( + signature('function two(renamed) { return renamed + renamed }'), + ) + expect(signature('function one(value) { return value + value }')).not.toBe( + signature('function one(value) { return value + other }'), + ) + expect(signature('function one(value) { return () => value }')).not.toBe( + signature('function one(value) { return (value) => value }'), + ) +}) diff --git a/test/vm/jsconfuser-vm/try-region.test.js b/test/vm/jsconfuser-vm/try-region.test.js new file mode 100644 index 00000000..74bb8e13 --- /dev/null +++ b/test/vm/jsconfuser-vm/try-region.test.js @@ -0,0 +1,52 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { decodeJsconfuserVm } from '../../../src/plugin/jsconfuser-vm.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +const fixture = (cell, name) => + fs.readFileSync( + new URL( + `../switch/fixtures/vm-switch-structured-emitter/try-region/${cell}/${name}`, + import.meta.url, + ), + 'utf8', + ) + +// The oracle is the source's own execution under the same host. +function observe(code) { + const window = {} + vm.runInNewContext( + code, + { window, performance: { now: () => 0 } }, + { timeout: 5000 }, + ) + return window.TEST_OUTPUT +} + +// forms-cff is the forms source under control flow flattening: there a try nested in a catch and +// its own catch jump to one shared jump step, the inner try's end, before the outer try's end. +for (const cell of ['routes', 'forms', 'forms-cff']) + test(`${cell}: every try form recovers natively with source behaviour`, () => { + const source = fixture(cell, 'source.js'), + encoded = fixture(cell, 'encoded.js') + const expected = observe(source) + expect(expected.length).toBeGreaterThan(0) + expect(observe(encoded)).toEqual(expected) + const result = decodeJsconfuserVm(encoded) + expect(result.ok, JSON.stringify(result.diagnostic)).toBe(true) + const ast = parseSuccessfulOutput(result.output) + expect(assertNoVmResidue(ast).ok).toBe(true) + expect(assertDerivedStateConsistency(ast, result.output).ok).toBe(true) + expect(result.output).not.toContain('__recovered_dispatch') + expect(observe(result.output)).toEqual(expected) + // A catch whose try body cannot throw never runs, so behaviour cannot show it was kept: + // its distinctive literal must survive in the recovered handler. + if (cell !== 'routes') + for (const literal of [41, 42]) + expect(result.output).toMatch(new RegExp(`= ${literal};`)) + }) From 9e6b61faddf3705d94f8d436bc4682da826fe907 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:28:18 +0100 Subject: [PATCH 12/12] test(vm/jsconfuser-vm): add the VM qualification harness Qualification encodes sources with the pinned encoder, retains the inputs with their provenance and checks the decoded output's behaviour, so a fixture can be regenerated and its claims reproduced from a recorded seed and a checked-in tool. Co-Authored-By: Claude Opus 5.5 Signed-off-by: echo094 <20028238+echo094@users.noreply.github.com> --- .gitignore | 4 + scripts/qualify-jsconfuser-vm/audit.mjs | 314 +++ .../qualify-jsconfuser-vm/capture_command.py | 72 + .../capture_native_replay.py | 196 ++ scripts/qualify-jsconfuser-vm/codec.mjs | 121 + scripts/qualify-jsconfuser-vm/config.mjs | 533 ++++ scripts/qualify-jsconfuser-vm/effects.mjs | 208 ++ .../qualify-jsconfuser-vm/fetch-encoder.mjs | 17 + scripts/qualify-jsconfuser-vm/finalize.mjs | 186 ++ scripts/qualify-jsconfuser-vm/generation.mjs | 135 + scripts/qualify-jsconfuser-vm/layout.mjs | 421 +++ scripts/qualify-jsconfuser-vm/oracles.mjs | 336 +++ scripts/qualify-jsconfuser-vm/patch.mjs | 129 + scripts/qualify-jsconfuser-vm/qualify.mjs | 856 ++++++ .../retain-minified-class-inputs.mjs | 206 ++ .../qualify-jsconfuser-vm/retain_evidence.py | 179 ++ .../test_capture_native_replay.py | 114 + .../test_retain_evidence.py | 52 + .../timing-observation.mjs | 277 ++ scripts/qualify-jsconfuser-vm/timing.mjs | 301 ++ scripts/qualify-jsconfuser-vm/worker.mjs | 96 + .../qualify-jsconfuser-vm/frontend.test.js | 160 ++ .../qualify-jsconfuser-vm/oracles.test.js | 62 + .../qualify-jsconfuser-vm/qualify.test.js | 848 ++++++ .../qualify-jsconfuser-vm/replay.test.js | 57 + .../qualify-jsconfuser-vm/timing.test.js | 66 + .../alias-handler-table-exceptions/encoded.js | 1231 +++++++++ .../qualification.json | 73 + .../alias-handler-table-exceptions/source.js | 1 + .../concealed-constants-arithmetic/encoded.js | 1019 +++++++ .../qualification.json | 57 + .../concealed-constants-arithmetic/source.js | 1 + .../concealed-constants-closures/encoded.js | 1019 +++++++ .../qualification.json | 66 + .../concealed-constants-closures/source.js | 1 + .../encoded.js | 1019 +++++++ .../qualification.json | 96 + .../source.js | 1 + .../encoded.js | 2420 +++++++++++++++++ .../qualification.json | 81 + .../source.js | 1 + .../encoded.js | 17 + .../qualification.json | 73 + .../source.js | 1 + .../encoded.js | 24 + .../qualification.json | 73 + .../source.js | 1 + .../fixtures/minified-runtime-boot/encoded.js | 15 + .../minified-runtime-boot/qualification.json | 57 + .../fixtures/minified-runtime-boot/source.js | 1 + .../minified-runtime-closures/encoded.js | 15 + .../qualification.json | 63 + .../minified-runtime-closures/source.js | 1 + .../minified-runtime-exceptions/encoded.js | 17 + .../qualification.json | 65 + .../minified-runtime-exceptions/source.js | 1 + .../minified-runtime-finally/encoded.js | 15 + .../qualification.json | 63 + .../minified-runtime-finally/source.js | 1 + .../fixtures/qualification-provenance.md | 30 + .../randomized-encoded-exceptions/encoded.js | 1021 +++++++ .../qualification.json | 73 + .../randomized-encoded-exceptions/source.js | 1 + .../fixtures/runtime-finalizers/baseline.js | 1019 +++++++ .../fixtures/runtime-finalizers/encoded.js | 1019 +++++++ .../runtime-finalizers/finalizers.src.js | 1 + .../runtime-finalizers/generation.json | 124 + .../fixtures/runtime-finalizers/oracle.json | 13 + .../fixtures/runtime-finalizers/patch.js | 1547 +++++++++++ .../fixtures/runtime-finalizers/table.js | 1028 +++++++ .../fixtures/runtime-finalizers/timing.js | 1019 +++++++ .../runtime-finalizers/timingEncoded.js | 1019 +++++++ .../runtime-finalizers/timingPatch.js | 1547 +++++++++++ .../runtime-finalizers/timingTable.js | 1028 +++++++ .../runtime-finalizers/top-level-finally.js | 1019 +++++++ .../top-level-finally.src.js | 1 + .../normalize-concealed-constants.test.js | 122 + .../jsconfuser-vm/patch-added-opcodes.test.js | 142 + .../qualification-provenance.test.js | 55 + .../vm/jsconfuser-vm/qualification-records.js | 41 + .../qualification-replay.test.js | 52 + .../runtime-finalizers.behavior.test.js | 186 ++ .../jsconfuser-vm/runtime-finalizers.test.js | 105 + vitest.config.js | 5 +- 84 files changed, 25751 insertions(+), 1 deletion(-) create mode 100644 scripts/qualify-jsconfuser-vm/audit.mjs create mode 100644 scripts/qualify-jsconfuser-vm/capture_command.py create mode 100644 scripts/qualify-jsconfuser-vm/capture_native_replay.py create mode 100644 scripts/qualify-jsconfuser-vm/codec.mjs create mode 100644 scripts/qualify-jsconfuser-vm/config.mjs create mode 100644 scripts/qualify-jsconfuser-vm/effects.mjs create mode 100644 scripts/qualify-jsconfuser-vm/fetch-encoder.mjs create mode 100644 scripts/qualify-jsconfuser-vm/finalize.mjs create mode 100644 scripts/qualify-jsconfuser-vm/generation.mjs create mode 100644 scripts/qualify-jsconfuser-vm/layout.mjs create mode 100644 scripts/qualify-jsconfuser-vm/oracles.mjs create mode 100644 scripts/qualify-jsconfuser-vm/patch.mjs create mode 100644 scripts/qualify-jsconfuser-vm/qualify.mjs create mode 100644 scripts/qualify-jsconfuser-vm/retain-minified-class-inputs.mjs create mode 100644 scripts/qualify-jsconfuser-vm/retain_evidence.py create mode 100644 scripts/qualify-jsconfuser-vm/test_capture_native_replay.py create mode 100644 scripts/qualify-jsconfuser-vm/test_retain_evidence.py create mode 100644 scripts/qualify-jsconfuser-vm/timing-observation.mjs create mode 100644 scripts/qualify-jsconfuser-vm/timing.mjs create mode 100644 scripts/qualify-jsconfuser-vm/worker.mjs create mode 100644 test/scripts/qualify-jsconfuser-vm/frontend.test.js create mode 100644 test/scripts/qualify-jsconfuser-vm/oracles.test.js create mode 100644 test/scripts/qualify-jsconfuser-vm/qualify.test.js create mode 100644 test/scripts/qualify-jsconfuser-vm/replay.test.js create mode 100644 test/scripts/qualify-jsconfuser-vm/timing.test.js create mode 100644 test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/qualification-provenance.md create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/qualification.json create mode 100644 test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/source.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/baseline.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/encoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/finalizers.src.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/generation.json create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/oracle.json create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/patch.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/table.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timing.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingEncoded.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingPatch.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingTable.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/top-level-finally.js create mode 100644 test/vm/jsconfuser-vm/fixtures/runtime-finalizers/top-level-finally.src.js create mode 100644 test/vm/jsconfuser-vm/normalize-concealed-constants.test.js create mode 100644 test/vm/jsconfuser-vm/patch-added-opcodes.test.js create mode 100644 test/vm/jsconfuser-vm/qualification-provenance.test.js create mode 100644 test/vm/jsconfuser-vm/qualification-records.js create mode 100644 test/vm/jsconfuser-vm/qualification-replay.test.js create mode 100644 test/vm/jsconfuser-vm/runtime-finalizers.behavior.test.js create mode 100644 test/vm/jsconfuser-vm/runtime-finalizers.test.js diff --git a/.gitignore b/.gitignore index a41616bf..6a3a0f0b 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,7 @@ input.js output.js .vscode/ .idea/ + +# Qualification workspace: encoder checkouts fetched by scripts/qualify-jsconfuser-vm +sandbox-tests/ +__pycache__/ diff --git a/scripts/qualify-jsconfuser-vm/audit.mjs b/scripts/qualify-jsconfuser-vm/audit.mjs new file mode 100644 index 00000000..2f5fa1f1 --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/audit.mjs @@ -0,0 +1,314 @@ +import fs from "node:fs"; +import path from "node:path"; +import crypto from "node:crypto"; +import { createRequire } from "node:module"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +export const decoderRoot = fileURLToPath(new URL("../../", import.meta.url)); +// The encoder checkout this decoder is qualified against. JSCONFUSER_VM_ENCODER names +// any checkout of it; otherwise fetch-encoder.mjs clones one into the ignored +// sandbox-tests/ workspace. Either way the snapshot refuses a HEAD other than PIN. +export const encoderRoot = process.env.JSCONFUSER_VM_ENCODER + ? path.resolve(process.env.JSCONFUSER_VM_ENCODER) + : path.join(decoderRoot, "sandbox-tests/js-confuser-vm"); +export const ENCODER_REPOSITORY = "https://github.com/MichaelXF/js-confuser-vm.git"; +// The JDK comes from JAVA_HOME, else from the first `java` on PATH that resolves +// into a JDK (one with a release file; launcher stubs do not). Null when none +// does, which only generation needs. +function findJavaRoot() { + if (process.env.JAVA_HOME) return process.env.JAVA_HOME; + for (const directory of (process.env.PATH ?? "").split(path.delimiter)) { + const candidate = path.join(directory, "java"); + if (!directory || !fs.existsSync(candidate)) continue; + const root = path.dirname(path.dirname(fs.realpathSync(candidate))); + if (fs.existsSync(path.join(root, "release"))) return root; + } + return null; +} +export const javaRoot = findJavaRoot(); +export const PIN = "20c5b96bf57337c568579352758d7685358650de"; +export const sha256 = (bytes) => + crypto.createHash("sha256").update(bytes).digest("hex"); +const parser = createRequire(path.join(decoderRoot, "package.json"))( + "@babel/parser", +); +export function manifest(root, censusRoot = root, ancestors = new Set()) { + const realRoot = fs.realpathSync(root); + if (ancestors.has(realRoot)) return []; + const nextAncestors = new Set([...ancestors, realRoot]); + // Finder writes .DS_Store into any folder it shows; it is never a qualification input or output. + return fs + .readdirSync(root, { withFileTypes: true }) + .filter((entry) => entry.name !== ".DS_Store") + .flatMap((entry) => { + const file = path.join(root, entry.name); + if (entry.isDirectory()) + return manifest(file, censusRoot, nextAncestors).map((record) => ({ + ...record, + path: `${entry.name}/${record.path}`, + })); + if (entry.isSymbolicLink()) { + const target = fs.readlinkSync(file), + resolved = fs.realpathSync(file); + const relative = path.relative(fs.realpathSync(censusRoot), resolved); + // pnpm targets within the census are already visited under their real paths. + const outside = + relative === ".." || + relative.startsWith(`..${path.sep}`) || + path.isAbsolute(relative); + const link = { + path: entry.name, + kind: "symlink", + target, + ...(outside ? { resolved } : { resolvedWithinCensus: relative }), + sha256: sha256(target), + }; + if (!outside) return [link]; + if (fs.statSync(file).isDirectory()) + return [ + link, + ...manifest(file, censusRoot, nextAncestors).map((record) => ({ + ...record, + path: `${entry.name}/${record.path}`, + })), + ]; + const bytes = fs.readFileSync(file); + return [{ ...link, bytes: bytes.length, contentSha256: sha256(bytes) }]; + } + if (!entry.isFile()) return []; + const bytes = fs.readFileSync(file); + return [{ path: entry.name, bytes: bytes.length, sha256: sha256(bytes) }]; + }) + .sort((a, b) => a.path.localeCompare(b.path)); +} +export function auditEntries(entries, { native = false, external = [] } = {}) { + const seen = new Set(), + specifiers = new Set(), + resolvedImports = []; + const allowed = new Set([ + "@babel/parser", + "@babel/traverse", + "@babel/types", + "@babel/generator", + "node:fs", + "node:crypto", + "vitest", + ...external, + ]); + const visit = (file) => { + file = path.resolve(file); + if (seen.has(file)) return; + seen.add(file); + const ast = parser.parse(fs.readFileSync(file, "utf8"), { + sourceType: "module", + plugins: file.endsWith(".ts") ? ["typescript"] : [], + }); + const walk = (node) => { + if (!node || typeof node !== "object") return; + if ( + [ + "ImportDeclaration", + "ExportNamedDeclaration", + "ExportAllDeclaration", + ].includes(node.type) && + node.source + ) { + const spec = node.source.value; + if (spec.startsWith(".")) { + const resolved = path.resolve(path.dirname(file), spec); + const babel = resolved.match( + /\/node_modules\/(@babel\/(?:parser|traverse|types|generator))\//, + )?.[1]; + if (babel) { + specifiers.add(babel); + resolvedImports.push({ + importer: path.relative(decoderRoot, file), + specifier: spec, + resolved: fs.realpathSync(resolved), + sha256: sha256(fs.readFileSync(resolved)), + }); + } else visit(resolved); + } else { + specifiers.add(spec); + if (!allowed.has(spec)) + throw new Error(`Unreviewed import ${spec} in ${file}`); + if (!spec.startsWith("node:")) { + const resolved = createRequire(file).resolve(spec); + if (path.isAbsolute(resolved)) + resolvedImports.push({ + importer: path.relative(decoderRoot, file), + specifier: spec, + resolved: fs.realpathSync(resolved), + sha256: sha256(fs.readFileSync(resolved)), + }); + } + } + } + if ( + !native && + (node.type === "ImportExpression" || + (node.type === "CallExpression" && node.callee?.type === "Import")) + ) + throw new Error(`Dynamic import in parser-only closure ${file}`); + if ( + !native && + ["CallExpression", "NewExpression"].includes(node.type) && + node.callee?.type === "Identifier" && + [ + "eval", + "Function", + "require", + "runInContext", + "runInNewContext", + "exec", + "execFile", + "spawn", + "execSync", + "execFileSync", + "spawnSync", + ].includes(node.callee.name) + ) + throw new Error(`Execution surface in parser-only closure ${file}`); + for (const child of Object.values(node)) + if (Array.isArray(child)) child.forEach(walk); + else if (child && typeof child === "object") walk(child); + }; + walk(ast); + }; + entries.forEach(visit); + return { + files: [...seen].sort().map((file) => path.relative(decoderRoot, file)), + external: [...specifiers].sort(), + resolvedImports, + }; +} +function git(root, args) { + return execFileSync("git", args, { + cwd: root, + encoding: "utf8", + timeout: 10000, + maxBuffer: 1 << 28, + }).trim(); +} +// Git identifies committed bytes, so a tracked tree is named by its commit and +// worktree status. Only uncommitted bytes, which Git does not hold, are digested. +function worktree(root, pathspecs) { + const status = git(root, [ + "status", + "--porcelain", + "--untracked-files=all", + "--", + ...pathspecs, + ]); + if (!status) return { status, uncommittedSha256: null }; + const untracked = git(root, [ + "ls-files", + "--others", + "--exclude-standard", + "--", + ...pathspecs, + ]) + .split("\n") + .filter(Boolean) + .sort(); + const delta = crypto.createHash("sha256"); + delta.update(git(root, ["diff", "--binary", "HEAD", "--", ...pathspecs])); + for (const file of untracked) + delta.update(`\0${file}\0`).update(fs.readFileSync(path.join(root, file))); + return { status, uncommittedSha256: delta.digest("hex") }; +} +// The qualifier's own files: its scripts and their tests, paths relative to the decoder. +export const toolDirectories = [ + "scripts/qualify-jsconfuser-vm", + "test/scripts/qualify-jsconfuser-vm", +]; +const toolPattern = /^(?:scripts|test\/scripts)\/qualify-jsconfuser-vm\/[^/]+\.(?:mjs|js)$/; +export function toolFiles() { + return toolDirectories + .flatMap((directory) => + fs + .readdirSync(path.join(decoderRoot, directory)) + .map((name) => `${directory}/${name}`), + ) + .filter((file) => toolPattern.test(file)) + .sort(); +} +// Qualifier tools changed between a commit and the current worktree. +export function changedTools(commit) { + const changed = [ + ...git(decoderRoot, [ + "diff", + "--name-only", + commit, + "--", + ...toolDirectories, + ]).split("\n"), + ...git(decoderRoot, [ + "ls-files", + "--others", + "--exclude-standard", + "--", + ...toolDirectories, + ]).split("\n"), + ]; + return [...new Set(changed.filter((file) => toolPattern.test(file)))].sort(); +} +export function snapshot({ generation = false } = {}) { + const encoderCommit = git(encoderRoot, ["rev-parse", "HEAD"]); + if (encoderCommit !== PIN) throw new Error("Encoder checkout/pin changed"); + const status = git(encoderRoot, [ + "status", + "--porcelain", + "--untracked-files=all", + ]); + if (status) throw new Error("Encoder checkout is not clean"); + const decoder = worktree(decoderRoot, ["src"]); + const files = toolFiles(); + const commit = git(decoderRoot, ["rev-parse", "HEAD"]); + const result = { + encoderCommit, + encoderWorktreeStatus: status, + // The qualifier shares the decoder's repository, so a commit to it moves the + // decoder commit; the src tree is what identifies the decoder under test. + decoderCommit: commit, + decoderSourceTree: git(decoderRoot, ["rev-parse", "HEAD:src"]), + decoderWorktreeStatus: decoder.status, + decoderUncommittedSha256: decoder.uncommittedSha256, + tools: { + commit, + files, + ...worktree(decoderRoot, files), + }, + node: { + version: process.version, + executable: path.basename(process.execPath), + sha256: sha256(fs.readFileSync(process.execPath)), + }, + host: { platform: process.platform, architecture: process.arch }, + decoderDependencies: manifest(path.join(decoderRoot, "node_modules")), + }; + if (generation) { + result.encoderDependencies = manifest( + path.join(encoderRoot, "node_modules"), + ); + if (!javaRoot) + throw new Error("Generation needs JAVA_HOME or java on PATH"); + result.java = { + executable: "java", + sha256: sha256(fs.readFileSync(path.join(javaRoot, "bin/java"))), + release: fs.readFileSync(path.join(javaRoot, "release"), "utf8"), + }; + result.jar = { + sha256: sha256( + fs.readFileSync( + path.join( + encoderRoot, + "node_modules/google-closure-compiler-java/compiler.jar", + ), + ), + ), + }; + } + return result; +} diff --git a/scripts/qualify-jsconfuser-vm/capture_command.py b/scripts/qualify-jsconfuser-vm/capture_command.py new file mode 100644 index 00000000..0ad99c7c --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/capture_command.py @@ -0,0 +1,72 @@ +"""Retain a bounded VM validation command's actual completion and exact streams.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import signal +import subprocess +from datetime import datetime, timezone + + +def utc(): + return datetime.now(timezone.utc).isoformat() + + +def identity(filename): + path = Path(filename).resolve(strict=True) + data = path.read_bytes() + return {"path": str(path), "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--capture", required=True) + parser.add_argument("--cwd", required=True) + parser.add_argument("--timeout", type=int, required=True) + parser.add_argument("--identity", action="append", default=[]) + parser.add_argument("argv", nargs=argparse.REMAINDER) + args = parser.parse_args() + argv = args.argv[1:] if args.argv[:1] == ["--"] else args.argv + if not argv or args.timeout <= 0: + parser.error("A command and positive timeout are required") + capture = Path(args.capture).resolve() + capture.mkdir(parents=True, exist_ok=False) + before = [identity(path) for path in args.identity] + start = {"argv": argv, "cwd": str(Path(args.cwd).resolve()), "startedAt": utc(), + "timeoutSeconds": args.timeout, "identities": before} + (capture / "start.json").write_text(json.dumps(start, indent=2) + "\n") + child = None + timed_out = False + failure = None + try: + with (capture / "stdout.log").open("xb") as out, (capture / "stderr.log").open("xb") as err: + child = subprocess.Popen(argv, cwd=args.cwd, stdout=out, stderr=err, start_new_session=True) + try: + child.wait(timeout=args.timeout) + except subprocess.TimeoutExpired: + timed_out = True + except BaseException as error: + failure = f"{type(error).__name__}: {error}" + finally: + if child and child.poll() is None: + try: + os.killpg(child.pid, signal.SIGKILL) + except ProcessLookupError: + pass + child.wait(timeout=10) + after = [identity(path) for path in args.identity] + code = child.returncode if child else None + result = {"completedAt": utc(), "exitCode": code if code is not None and code >= 0 else None, + "signal": -code if code is not None and code < 0 else None, "timedOut": timed_out, + "failure": failure, "identitiesUnchanged": before == after, + "identitiesAfter": after, "stdout": identity(capture / "stdout.log"), + "stderr": identity(capture / "stderr.log")} + (capture / "result.json").write_text(json.dumps(result, indent=2) + "\n") + print(json.dumps(result)) + if code != 0 or timed_out or failure or before != after: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/qualify-jsconfuser-vm/capture_native_replay.py b/scripts/qualify-jsconfuser-vm/capture_native_replay.py new file mode 100644 index 00000000..12546e57 --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/capture_native_replay.py @@ -0,0 +1,196 @@ +"""Capture one reviewed frozen-input native replay without reusing output paths. + +Run from the decoder root, which the replay argv is relative to. +""" + +import argparse +import hashlib +import json +import os +import platform +import resource +import shutil +import signal +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + + +def utc(): + return datetime.now(timezone.utc).isoformat() + + +def identity(path): + path = Path(path).resolve(strict=True) + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return {"path": str(path), "bytes": path.stat().st_size, "sha256": digest.hexdigest()} + + +def write_new(path, value): + with path.open("x") as stream: + json.dump(value, stream, indent=2) + stream.write("\n") + + +def overlaps(left, right): + return left == right or left in right.parents or right in left.parents + + +class ReplayCancelled(BaseException): + pass + + +def stop_child(child): + """Bound cleanup and retain termination races instead of losing final metadata.""" + errors = [] + returncode = None + if child is None: + return returncode, errors + try: + returncode = child.poll() + except Exception as error: + errors.append(f"poll: {type(error).__name__}: {error}") + if returncode is not None: + return returncode, errors + for signum in [signal.SIGTERM, signal.SIGKILL]: + try: + os.killpg(child.pid, signum) + except ProcessLookupError: + pass + except Exception as error: + errors.append(f"killpg({signum}): {type(error).__name__}: {error}") + try: + returncode = child.wait(timeout=5) + break + except Exception as error: + errors.append(f"wait({signum}): {type(error).__name__}: {error}") + return returncode, errors + + +def optional_identity(path): + try: + return identity(path) if path.is_file() else None + except Exception as error: + return {"error": f"{type(error).__name__}: {error}"} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--replay", required=True) + parser.add_argument("--output", required=True) + parser.add_argument("--capture", required=True) + parser.add_argument("--identity", action="append", required=True) + args = parser.parse_args() + cwd = Path.cwd().resolve() + replay = Path(args.replay).resolve(strict=True) + output = Path(args.output).resolve() + capture = Path(args.capture).resolve() + if not replay.is_dir() or overlaps(replay, output) or overlaps(replay, capture): + raise ValueError("Replay input must be a directory disjoint from output and capture") + if overlaps(output, capture): + raise ValueError("Output and capture must be disjoint") + if Path(args.output).exists() or Path(args.output).is_symlink(): + raise FileExistsError("Output must be physically absent; failed attempts cannot be reused") + if Path(args.capture).exists() or Path(args.capture).is_symlink(): + raise FileExistsError("Capture must be physically absent") + node = Path(shutil.which("node") or "").resolve(strict=True) + before = [identity(path) for path in args.identity] + runtime = identity(node) + version = subprocess.check_output([str(node), "--version"], text=True).strip() + argv = [str(node), "scripts/qualify-jsconfuser-vm/qualify.mjs", "--replay", str(replay), + "--replay-mode", "native", "--output", str(output)] + capture.mkdir(parents=True, exist_ok=False) + start = { + "argv": argv, "cwd": str(cwd), "utcStart": utc(), + "host": platform.node(), + "os": {"system": platform.system(), "release": platform.release(), "version": platform.version()}, + "architecture": platform.machine(), + "runtime": runtime, "runtimeVersion": version, "python": identity(sys.executable), + "identitiesBefore": before, + "environment": {key: os.environ.get(key) for key in + ["PATH", "NODE_OPTIONS", "JAVA_HOME", "LANG", "LC_ALL", "TZ"]}, + "execution": "Serial reviewed core replay; child timeouts unchanged; no outer timeout", + } + write_new(capture / "execution-start.json", start) + child = None + interrupted = None + failure = None + returncode = None + cleanup_errors = [] + signals = [] + finalizing = False + + def cancel(signum, _frame): + signals.append({"signal": signum, "utc": utc()}) + if not finalizing and len(signals) == 1: + raise ReplayCancelled(signum) + + previous_handlers = {number: signal.signal(number, cancel) + for number in [signal.SIGINT, signal.SIGTERM, signal.SIGHUP]} + try: + with (capture / "stdout.log").open("xb") as stdout, (capture / "stderr.log").open("xb") as stderr: + child = subprocess.Popen(argv, cwd=cwd, stdout=stdout, stderr=stderr, start_new_session=True) + returncode = child.wait() + except ReplayCancelled as error: + interrupted = f"Signal {error.args[0]}" + except KeyboardInterrupt: + interrupted = "KeyboardInterrupt" + except Exception as error: + failure = f"{type(error).__name__}: {error}" + except BaseException as error: + failure = f"{type(error).__name__}: {error}" + finally: + finalizing = True + if returncode is None: + returncode, cleanup_errors = stop_child(child) + after = [] + identity_failure = None + try: + after = [identity(path) for path in args.identity] + except Exception as error: + identity_failure = f"{type(error).__name__}: {error}" + inventory = [] + inventory_failure = None + try: + if output.is_dir(): + for path in sorted(output.rglob("*")): + if path.is_symlink(): + inventory.append({"path": str(path.relative_to(output)), "type": "symlink", + "target": os.readlink(path)}) + elif path.is_file(): + inventory.append({"path": str(path.relative_to(output)), "bytes": path.stat().st_size}) + except Exception as error: + inventory_failure = f"{type(error).__name__}: {error}" + result = { + "utcEnd": utc(), "observedReturnCode": returncode, + "observedExitCode": returncode if returncode is not None and returncode >= 0 else None, + "observedSignal": -returncode if returncode is not None and returncode < 0 else None, + "interruption": interrupted, "wrapperSignals": signals, + "launchOrWaitFailure": failure, "cleanupErrors": cleanup_errors, + "childTerminationConfirmed": returncode is not None if child is not None else None, + "outerTimeout": None, "oom": "Not independently classified; inspect retained stderr and signal", + "identitiesAfter": after, "identitiesUnchanged": before == after and identity_failure is None, + "identityReadFailure": identity_failure, + "publication": {"output": str(output), "members": inventory, "inventoryFailure": inventory_failure}, + "childPeakRssPlatformUnits": resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss, + "stdout": optional_identity(capture / "stdout.log"), + "stderr": optional_identity(capture / "stderr.log"), + } + write_new(capture / "execution-result.json", result) + for number, handler in previous_handlers.items(): + signal.signal(number, handler) + print(json.dumps({"capture": str(capture), "observedReturnCode": returncode, + "identitiesUnchanged": result["identitiesUnchanged"]})) + if interrupted or signals or failure or cleanup_errors or inventory_failure or not result["identitiesUnchanged"]: + status = 1 + else: + status = returncode if returncode is not None and returncode >= 0 else 1 + return status + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/qualify-jsconfuser-vm/codec.mjs b/scripts/qualify-jsconfuser-vm/codec.mjs new file mode 100644 index 00000000..41d70767 --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/codec.mjs @@ -0,0 +1,121 @@ +export const CODEC_VERSION = "vm-qualification-lossless.v2"; + +export function tagged(value, active = new Set()) { + if (value === undefined) return ["undefined"]; + if (value === null) return ["null"]; + if (typeof value === "number") { + if (Number.isNaN(value)) return ["number", "NaN"]; + if (Object.is(value, -0)) return ["number", "-0"]; + if (!Number.isFinite(value)) return ["number", String(value)]; + return ["number", value]; + } + if (["string", "boolean"].includes(typeof value)) + return [typeof value, value]; + if (typeof value === "bigint") return ["bigint", String(value)]; + if (typeof value !== "object") + throw new TypeError("Unsupported qualification value"); + if (active.has(value)) throw new TypeError("Cyclic qualification value"); + active.add(value); + const tag = (child) => tagged(child, active); + if (Object.getOwnPropertySymbols(value).length) + throw new TypeError("Symbol qualification key"); + const properties = Object.getOwnPropertyNames(value) + .sort() + .map((key) => { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (!Object.hasOwn(descriptor, "value")) + throw new TypeError("Accessor qualification property"); + return [ + key, + tag(descriptor.value), + descriptor.enumerable, + descriptor.writable, + descriptor.configurable, + ]; + }); + let result; + if (Array.isArray(value)) result = ["array", value.length, properties]; + else if (Object.prototype.toString.call(value) === "[object Map]") + result = [ + "map", + [...value].map(([key, item]) => [tag(key), tag(item)]), + properties, + ]; + else if (Object.prototype.toString.call(value) === "[object Set]") + result = ["set", [...value].map(tag), properties]; + else if (Object.prototype.toString.call(value) === "[object Object]") { + const prototype = Object.getPrototypeOf(value); + if (prototype !== null && prototype.constructor?.name !== "Object") + throw new TypeError("Opaque qualification prototype"); + result = ["object", properties, prototype === null]; + } else throw new TypeError("Opaque qualification value"); + active.delete(value); + return result; +} + +// Equality is the declared own-string-key/value grammar; frozen owned records +// and ordinary expected records compare by the same values. Serialization also +// preserves each supported data descriptor's attributes for exact readback. +function valueGrammar(record) { + const [kind, payload, extra] = record; + const properties = (items) => + items.map(([key, value]) => [key, valueGrammar(value)]); + if (kind === "array") return [kind, payload, properties(extra)]; + if (kind === "object") return [kind, properties(payload), extra]; + if (kind === "map") + return [ + kind, + payload.map(([key, value]) => [valueGrammar(key), valueGrammar(value)]), + properties(extra), + ]; + if (kind === "set") + return [kind, payload.map(valueGrammar), properties(extra)]; + return record; +} +export const exactEqual = (left, right) => + JSON.stringify(valueGrammar(tagged(left))) === + JSON.stringify(valueGrammar(tagged(right))); +// Records are compact: every reader parses them, so indentation would only cost disk. +export const stringifyRecord = (value) => + `${JSON.stringify({ codec: CODEC_VERSION, value: tagged(value) })}\n`; + +export function untagged(value) { + const [kind, payload] = value; + if (kind === "undefined") return undefined; + if (kind === "null") return null; + if (kind === "number") + return payload === "-0" + ? -0 + : typeof payload === "number" + ? payload + : Number(payload); + if (kind === "string" || kind === "boolean") return payload; + if (kind === "bigint") return BigInt(payload); + const restore = (object, properties) => { + for (const [key, item, enumerable, writable, configurable] of properties) + Object.defineProperty(object, key, { + value: untagged(item), + enumerable, + writable, + configurable, + }); + return object; + }; + if (kind === "array") return restore(new Array(payload), value[2]); + if (kind === "object") + return restore(value[2] ? Object.create(null) : {}, payload); + if (kind === "map") + return restore( + new Map(payload.map(([key, item]) => [untagged(key), untagged(item)])), + value[2], + ); + if (kind === "set") return restore(new Set(payload.map(untagged)), value[2]); + throw new TypeError("Unknown qualification tag"); +} + +export function parseRecord(text) { + const envelope = JSON.parse(text); + if (envelope.codec !== CODEC_VERSION) + throw new TypeError("Unknown qualification codec"); + return untagged(envelope.value); +} diff --git a/scripts/qualify-jsconfuser-vm/config.mjs b/scripts/qualify-jsconfuser-vm/config.mjs new file mode 100644 index 00000000..0ad1843f --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/config.mjs @@ -0,0 +1,533 @@ +export const sources = { + patchArrays: "const a = [1, 2, 3, 4]; const b = [5, 6, 7, 8]; const c = [9, 10, 11, 12]; const d = [13, 14, 15, 16]; const e = [17, 18, 19, 20]; const f = [21, 22, 23, 24]; window.TEST_OUTPUT = [a, b, c, d, e, f];", + spread: "function sum(a, b) { return a + b; } const args = [2, 5]; window.TEST_OUTPUT = sum(...args);", + arithmetic: "window.TEST_OUTPUT = (6 * 7 + 3) % 5;", + branches: + "let value = 0; for (let i = 0; i < 4; i++) { if (i % 2) value += i; else value -= i; } window.TEST_OUTPUT = value;", + closures: + "function counter(start) { let value = start; return () => ++value; } const next = counter(3); window.TEST_OUTPUT = [next(), next()];", + nestedStrings: + 'function outer(prefix) { return function middle(n) { return function inner() { return prefix + ":" + n; }; }; } window.TEST_OUTPUT = outer("id")(7)();', + collections: + "const object = { a: 2, b: 5 }; const list = [object.a, object.b]; window.TEST_OUTPUT = list.map((x) => x * 2);", + repeatedArrays: + "const a = [1, 2]; const b = [3, 4]; const c = [5, 6]; window.TEST_OUTPUT = [a, b, c];", + switch: + 'function classify(value) { switch (value) { case 1: return "one"; case 2: return "two"; default: return "other"; } } window.TEST_OUTPUT = [classify(1), classify(3)];', + forIn: + "const object = { a: 2, b: 5 }; let sum = 0; for (const key in object) sum += object[key]; window.TEST_OUTPUT = sum;", + methods: + "const object = { value: 3, double() { return this.value * 2; } }; window.TEST_OUTPUT = object.double();", + tryCatch: + 'function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } } window.TEST_OUTPUT = [check(-1), check(4)];', + throwCatch: + 'function check() { try { throw new Error("caught"); } catch (error) { return error.message; } } window.TEST_OUTPUT = check();', + tryFinally: + "let count = 0; function check(value) { try { return value + 1; } finally { count++; } } window.TEST_OUTPUT = [check(4), count];", + exceptions: + 'function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished];', + exceptionRoutes: + 'const trace = []; function make() { let count = 0; return function run(mode) { try { trace.push("try:" + mode); if (mode === 0) JSON.parse("{"); if (mode === 1) { const value = null; value.missing; } if (mode === 2) throw new Error("explicit"); return ++count; } catch (error) { trace.push("catch:" + error.name); return ++count; } finally { trace.push("finally:" + mode); } }; } const run = make(); window.TEST_OUTPUT = [run(0), run(1), run(2), run(3), trace];', +}; + +const representationOptions = [ + "randomizeOpcodes", + "shuffleOpcodes", + "encodeBytecode", + "concealConstants", + "controlFlowFlattening", + "dispatcher", + "stringConcealing", + "macroOpcodes", + "specializedOpcodes", + "aliasedOpcodes", + "antiInstrumentation", + "selfModifying", + "timingChecks", + "classObfuscation", + "handlerTable", + "minify", +]; + +export const baselineOptions = Object.fromEntries( + representationOptions.map((option) => [option, false]), +); +baselineOptions.target = "browser"; + +export const profiles = { + baseline: {}, + randomizeOpcodesClassObfuscation: { randomizeOpcodes: true, classObfuscation: true }, + randomizeOpcodesClassObfuscationMinify: { randomizeOpcodes: true, classObfuscation: true, minify: true }, + randomizeOpcodesMinify: { randomizeOpcodes: true, minify: true }, + ...Object.fromEntries( + representationOptions.map((option) => [option, { [option]: true }]), + ), + stringConcealingControlFlowFlattening: { + stringConcealing: true, + controlFlowFlattening: true, + }, + stringConcealingDispatcher: { + stringConcealing: true, + dispatcher: true, + }, + controlFlowFlatteningDispatcher: { + controlFlowFlattening: true, + dispatcher: true, + }, + stringConcealingControlFlowFlatteningDispatcher: { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + }, + stringConcealingConcealConstants: { + stringConcealing: true, + concealConstants: true, + }, + controlFlowFlatteningConcealConstants: { + controlFlowFlattening: true, + concealConstants: true, + }, + dispatcherConcealConstants: { + dispatcher: true, + concealConstants: true, + }, + stringConcealingControlFlowFlatteningConcealConstants: { + stringConcealing: true, + controlFlowFlattening: true, + concealConstants: true, + }, + stringConcealingDispatcherConcealConstants: { + stringConcealing: true, + dispatcher: true, + concealConstants: true, + }, + controlFlowFlatteningDispatcherConcealConstants: { + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + }, + stringConcealingControlFlowFlatteningDispatcherConcealConstants: { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + }, + concealConstantsAntiInstrumentation: { + concealConstants: true, + antiInstrumentation: true, + }, + stringConcealingAntiInstrumentation: { + stringConcealing: true, + antiInstrumentation: true, + }, + controlFlowFlatteningAntiInstrumentation: { + controlFlowFlattening: true, + antiInstrumentation: true, + }, + dispatcherAntiInstrumentation: { + dispatcher: true, + antiInstrumentation: true, + }, + antiInstrumentationSpecializedOpcodes: { + antiInstrumentation: true, + specializedOpcodes: true, + }, + antiInstrumentationMacroOpcodes: { + antiInstrumentation: true, + macroOpcodes: true, + }, + specializedOpcodesMacroOpcodes: { + specializedOpcodes: true, + macroOpcodes: true, + }, + antiInstrumentationSpecializedOpcodesMacroOpcodes: { + antiInstrumentation: true, + specializedOpcodes: true, + macroOpcodes: true, + }, + macroOpcodesAliasedOpcodes: { + macroOpcodes: true, + aliasedOpcodes: true, + }, + aliasedOpcodesShuffleOpcodes: { + aliasedOpcodes: true, + shuffleOpcodes: true, + }, + aliasedOpcodesHandlerTable: { + aliasedOpcodes: true, + handlerTable: true, + }, + shuffleOpcodesHandlerTable: { + shuffleOpcodes: true, + handlerTable: true, + }, + classObfuscationHandlerTable: { + classObfuscation: true, + handlerTable: true, + }, + encodeBytecodeClassObfuscation: { + encodeBytecode: true, + classObfuscation: true, + }, + encodeBytecodeHandlerTable: { + encodeBytecode: true, + handlerTable: true, + }, + encodeBytecodeClassObfuscationHandlerTable: { + encodeBytecode: true, + classObfuscation: true, + handlerTable: true, + }, + randomizeOpcodesHandlerTable: { + randomizeOpcodes: true, + handlerTable: true, + }, + randomizeOpcodesEncodeBytecode: { + randomizeOpcodes: true, + encodeBytecode: true, + }, + randomizeOpcodesClassObfuscation: { + randomizeOpcodes: true, + classObfuscation: true, + }, + aliasedOpcodesClassObfuscation: { + aliasedOpcodes: true, + classObfuscation: true, + }, + aliasedOpcodesHandlerTableClassObfuscation: { + aliasedOpcodes: true, + handlerTable: true, + classObfuscation: true, + }, + specializedOpcodesClassObfuscation: { + specializedOpcodes: true, + classObfuscation: true, + }, + macroOpcodesClassObfuscation: { + macroOpcodes: true, + classObfuscation: true, + }, + antiInstrumentationClassObfuscation: { + antiInstrumentation: true, + classObfuscation: true, + }, + aliasedOpcodesShuffleOpcodesHandlerTable: { + aliasedOpcodes: true, + shuffleOpcodes: true, + handlerTable: true, + }, + antiInstrumentationAliasedOpcodes: { + antiInstrumentation: true, + aliasedOpcodes: true, + }, + specializedOpcodesAliasedOpcodes: { + specializedOpcodes: true, + aliasedOpcodes: true, + }, + antiInstrumentationMacroOpcodesAliasedOpcodes: { + antiInstrumentation: true, + macroOpcodes: true, + aliasedOpcodes: true, + }, + specializedOpcodesMacroOpcodesAliasedOpcodes: { + specializedOpcodes: true, + macroOpcodes: true, + aliasedOpcodes: true, + }, + antiInstrumentationSpecializedOpcodesAliasedOpcodes: { + antiInstrumentation: true, + specializedOpcodes: true, + aliasedOpcodes: true, + }, + antiInstrumentationSpecializedOpcodesMacroOpcodesAliasedOpcodes: { + antiInstrumentation: true, + specializedOpcodes: true, + macroOpcodes: true, + aliasedOpcodes: true, + }, + stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodesAliasedOpcodes: + { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + specializedOpcodes: true, + macroOpcodes: true, + aliasedOpcodes: true, + }, + stringConcealingControlFlowFlatteningDispatcherAntiInstrumentation: { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + antiInstrumentation: true, + }, + stringConcealingControlFlowFlatteningConcealConstantsAntiInstrumentation: { + stringConcealing: true, + controlFlowFlattening: true, + concealConstants: true, + antiInstrumentation: true, + }, + stringConcealingDispatcherConcealConstantsAntiInstrumentation: { + stringConcealing: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + }, + controlFlowFlatteningDispatcherConcealConstantsAntiInstrumentation: { + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + }, + stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentation: + { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + }, + controlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodes: + { + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + specializedOpcodes: true, + }, + stringConcealingDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodes: + { + stringConcealing: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + specializedOpcodes: true, + }, + stringConcealingControlFlowFlatteningConcealConstantsAntiInstrumentationSpecializedOpcodes: + { + stringConcealing: true, + controlFlowFlattening: true, + concealConstants: true, + antiInstrumentation: true, + specializedOpcodes: true, + }, + stringConcealingControlFlowFlatteningDispatcherAntiInstrumentationSpecializedOpcodes: + { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + antiInstrumentation: true, + specializedOpcodes: true, + }, + stringConcealingControlFlowFlatteningDispatcherConcealConstantsSpecializedOpcodes: + { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + specializedOpcodes: true, + }, + stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodes: + { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + specializedOpcodes: true, + }, + stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationMacroOpcodes: + { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + macroOpcodes: true, + }, + stringConcealingControlFlowFlatteningDispatcherConcealConstantsSpecializedOpcodesMacroOpcodes: + { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + specializedOpcodes: true, + macroOpcodes: true, + }, + stringConcealingControlFlowFlatteningDispatcherAntiInstrumentationSpecializedOpcodesMacroOpcodes: + { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + antiInstrumentation: true, + specializedOpcodes: true, + macroOpcodes: true, + }, + stringConcealingControlFlowFlatteningConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodes: + { + stringConcealing: true, + controlFlowFlattening: true, + concealConstants: true, + antiInstrumentation: true, + specializedOpcodes: true, + macroOpcodes: true, + }, + stringConcealingDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodes: + { + stringConcealing: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + specializedOpcodes: true, + macroOpcodes: true, + }, + controlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodes: + { + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + specializedOpcodes: true, + macroOpcodes: true, + }, + stringConcealingControlFlowFlatteningDispatcherConcealConstantsAntiInstrumentationSpecializedOpcodesMacroOpcodes: + { + stringConcealing: true, + controlFlowFlattening: true, + dispatcher: true, + concealConstants: true, + antiInstrumentation: true, + specializedOpcodes: true, + macroOpcodes: true, + }, +}; + +const eightOptionAliasStack = [ + "stringConcealing", + "controlFlowFlattening", + "dispatcher", + "concealConstants", + "antiInstrumentation", + "specializedOpcodes", + "macroOpcodes", + "aliasedOpcodes", +]; +for (const omitted of eightOptionAliasStack.filter( + (option) => option !== "aliasedOpcodes", +)) { + profiles[ + `eightOptionAliasStackWithout${omitted[0].toUpperCase()}${omitted.slice(1)}` + ] = Object.fromEntries( + eightOptionAliasStack + .filter((option) => option !== omitted) + .map((option) => [option, true]), + ); +} + +const nineOptionAliasShuffleStack = [ + ...eightOptionAliasStack, + "shuffleOpcodes", +]; +profiles.nineOptionAliasShuffleStack = Object.fromEntries( + nineOptionAliasShuffleStack.map((option) => [option, true]), +); +for (const omitted of nineOptionAliasShuffleStack.filter( + (option) => option !== "shuffleOpcodes", +)) { + profiles[ + `nineOptionAliasShuffleStackWithout${omitted[0].toUpperCase()}${omitted.slice(1)}` + ] = Object.fromEntries( + nineOptionAliasShuffleStack + .filter((option) => option !== omitted) + .map((option) => [option, true]), + ); +} + +const tenOptionAliasShuffleHandlerStack = [ + ...nineOptionAliasShuffleStack, + "handlerTable", +]; +profiles.tenOptionAliasShuffleHandlerStack = Object.fromEntries( + tenOptionAliasShuffleHandlerStack.map((option) => [option, true]), +); +for (const omitted of tenOptionAliasShuffleHandlerStack.filter( + (option) => option !== "handlerTable", +)) { + profiles[ + `tenOptionAliasShuffleHandlerStackWithout${omitted[0].toUpperCase()}${omitted.slice(1)}` + ] = Object.fromEntries( + tenOptionAliasShuffleHandlerStack + .filter((option) => option !== omitted) + .map((option) => [option, true]), + ); +} + +const elevenOptionClassStack = [ + ...tenOptionAliasShuffleHandlerStack, + "classObfuscation", +]; +profiles.elevenOptionClassStack = Object.fromEntries( + elevenOptionClassStack.map((option) => [option, true]), +); +for (const omitted of elevenOptionClassStack.filter( + (option) => option !== "classObfuscation", +)) { + profiles[ + `elevenOptionClassStackWithout${omitted[0].toUpperCase()}${omitted.slice(1)}` + ] = Object.fromEntries( + elevenOptionClassStack + .filter((option) => option !== omitted) + .map((option) => [option, true]), + ); +} + +const twelveOptionEncodedStack = [...elevenOptionClassStack, "encodeBytecode"]; +profiles.twelveOptionEncodedStack = Object.fromEntries( + twelveOptionEncodedStack.map((option) => [option, true]), +); +for (const omitted of twelveOptionEncodedStack) { + profiles[ + `twelveOptionEncodedStackWithout${omitted[0].toUpperCase()}${omitted.slice(1)}` + ] = Object.fromEntries( + twelveOptionEncodedStack + .filter((option) => option !== omitted) + .map((option) => [option, true]), + ); +} + +profiles.selfModifyingEncodeBytecode = { + selfModifying: true, + encodeBytecode: true, +}; +for (const option of [ + "aliasedOpcodes", + "macroOpcodes", + "specializedOpcodes", + "antiInstrumentation", + "classObfuscation", + "handlerTable", +]) { + profiles[`selfModifying${option[0].toUpperCase()}${option.slice(1)}`] = { + selfModifying: true, + [option]: true, + }; +} +profiles.twelveOptionSelfModifyingStack = Object.fromEntries( + [...twelveOptionEncodedStack, "selfModifying"].map((option) => [ + option, + true, + ]), +); +for (const option of ["encodeBytecode", "classObfuscation", "handlerTable"]) { + profiles[`minify${option[0].toUpperCase()}${option.slice(1)}`] = { + minify: true, + [option]: true, + }; +} +profiles.minifySelfModifying = { minify: true, selfModifying: true }; +profiles.minifySelfModifyingEncodeBytecode = { + minify: true, + selfModifying: true, + encodeBytecode: true, +}; diff --git a/scripts/qualify-jsconfuser-vm/effects.mjs b/scripts/qualify-jsconfuser-vm/effects.mjs new file mode 100644 index 00000000..a2777a2f --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/effects.mjs @@ -0,0 +1,208 @@ +import { parse } from '../../node_modules/@babel/parser/lib/index.js'; +import { classLayoutEffect } from "./layout.mjs"; +import { profiles } from "./config.mjs"; +import { exactEqual as same } from "./codec.mjs"; +import { extractContainerFromSource } from "../../src/vm/jsconfuser-vm/extract-container.js"; +import { diagnoseSelfModifyingRegions } from "../../src/vm/jsconfuser-vm/observe-self-modifying.js"; +const patchEffects = new Map(); +function hasProvedPatch(source) { + if (patchEffects.has(source)) return patchEffects.get(source); + const container = extractContainerFromSource(source); + const diagnosed = container ? diagnoseSelfModifyingRegions(container) : null; + const observed = diagnosed?.ok + ? true + : diagnosed?.diagnostic?.code === "no-patch" + ? false + : null; + patchEffects.set(source, observed); + return observed; +} + +// Representation census only: no admission or numeric-role inference. The +// removal control must contain a complete distinct label set too. +function opcodeLabels(source) { + try { + const ast = parse(source), sets = []; + const visit = node => { + if(!node || typeof node !== 'object') return; + if(node.type==='SwitchStatement') { + const labels=node.cases.filter(c=>c.test!==null).map(c=>c.test?.type==='NumericLiteral'?c.test.value:null); + if(labels.length===61 && labels.every(Number.isSafeInteger) && new Set(labels).size===61) sets.push(labels.sort((a,b)=>a-b)); + } + if(node.type==='VariableDeclarator' && node.id?.name==='OP' && node.init?.type==='ObjectExpression') { + const labels=node.init.properties.map(p=>p.value?.type==='NumericLiteral'?p.value.value:null); + if(labels.length===61 && labels.every(Number.isSafeInteger) && new Set(labels).size===61) sets.push(labels.sort((a,b)=>a-b)); + } + for(const [key,value]of Object.entries(node)) if(!['loc','start','end','extra'].includes(key)) { + if(Array.isArray(value))value.forEach(visit);else if(value?.type)visit(value); + } + }; + visit(ast); return sets.length===1 ? sets[0] : null; + } catch { return null; } +} +const randomizationEffect=(left,right)=>{ + const before=opcodeLabels(left),after=opcodeLabels(right); + return before&&after ? !same(before,after) : null; +}; + +export function emittedOptionEffect(profile, baseline, candidate, peers) { + if (profile === "baseline") return { axis: "reference", observed: null }; + if (!baseline || !candidate) return { axis: "uncompared", observed: null }; + const bytecode = (source) => + source.match(/^var BYTECODE = ([^\n]+);$/m)?.[1] ?? + source.match(/\bdecodeBytecode\(("[A-Za-z0-9+/=]+")\)/)?.[1] ?? + null; + const switchCases = (source) => + [...source.matchAll(/\bcase OP\.([A-Z_]+):/g)].map((match) => match[1]); + const handlerKeys = (source) => + [...source.matchAll(/VMPrototype\[(OP\.[A-Z_]+|\d+)\]\s*=/g)].map( + (match) => match[1], + ); + const minifiedTableKeys = (source) => + [ + ...source.matchAll(/\b[A-Za-z_$][\w$]*\[(\d+)\]\s*=\s*function\s*\(/g), + ].map((match) => Number(match[1])); + const enabled = Object.keys(profiles[profile]).filter( + (option) => profiles[profile][option], + ); + if (enabled.length > 1) { + const components = Object.fromEntries( + enabled.map((option) => { + const counterpartOptions = enabled.filter((item) => item !== option); + const counterpartName = Object.keys(profiles).find((name) => + same( + Object.keys(profiles[name]) + .filter((key) => profiles[name][key]) + .sort(), + counterpartOptions.sort(), + ), + ); + const counterpart = peers.get(counterpartName); + if(counterpart && option === "randomizeOpcodes") return [option,randomizationEffect(counterpart,candidate)]; + if (counterpart && option === "minify") { + return [ + option, + Buffer.byteLength(candidate) < Buffer.byteLength(counterpart), + ]; + } + if ( + counterpart && + option === "selfModifying" && + enabled.includes("minify") + ) { + const candidatePatch = hasProvedPatch(candidate); + const counterpartPatch = hasProvedPatch(counterpart); + return [ + option, + candidatePatch === null || counterpartPatch === null + ? null + : candidatePatch && !counterpartPatch, + ]; + } + if ( + counterpart && + option === "handlerTable" && + enabled.includes("minify") + ) { + return [ + option, + minifiedTableKeys(candidate).length >= 61 && + minifiedTableKeys(counterpart).length === 0, + ]; + } + if ( + counterpart && + option === "encodeBytecode" && + enabled.includes("minify") + ) { + return [ + option, + candidate.includes("new Uint32Array(") && + !counterpart.includes("new Uint32Array("), + ]; + } + return [ + option, + counterpart + ? option === "classObfuscation" + ? classLayoutEffect(counterpart, candidate).observed + : option === "handlerTable" + ? counterpart.includes("switch (op)") && + !candidate.includes("switch (op)") + : option === "shuffleOpcodes" + ? candidate.includes("switch (op)") && + counterpart.includes("switch (op)") + ? switchCases(candidate).length && + switchCases(counterpart).length + ? !same(switchCases(candidate), switchCases(counterpart)) + : null + : handlerKeys(candidate).length && + handlerKeys(counterpart).length + ? !same(handlerKeys(candidate), handlerKeys(counterpart)) + : null + : bytecode(candidate) && bytecode(counterpart) + ? bytecode(candidate) !== bytecode(counterpart) + : null + : null, + ]; + }), + ); + return { + axis: enabled.some((option) => + ["shuffleOpcodes", "handlerTable", "classObfuscation"].includes(option), + ) + ? "emitted-representation-components" + : "serialized-bytecode-components", + observed: Object.values(components).includes(false) + ? false + : Object.values(components).includes(null) + ? null + : true, + components, + ...(enabled.includes("classObfuscation") + ? { + classWitness: classLayoutEffect( + peers.get( + enabled.length === 2 && enabled.includes("minify") + ? "minify" + : "baseline", + ) ?? "", + candidate, + ), + } + : {}), + }; + } + let axis; + let observed; + if (profile === "shuffleOpcodes") { + axis = "runtime-opcode-order"; + const left = switchCases(baseline); + const right = switchCases(candidate); + observed = left.length && right.length ? !same(left, right) : null; + } else if (profile === "randomizeOpcodes") { + axis="source-opcode-label-allocation"; + observed=randomizationEffect(baseline,candidate); + } else if (profile === "classObfuscation") { + axis = "runtime-class-layout"; + observed = classLayoutEffect(baseline, candidate).observed; + } else if (profile === "handlerTable") { + axis = "runtime-handler-table"; + observed = + baseline.includes("switch (op)") && !candidate.includes("switch (op)"); + } else if (profile === "timingChecks") { + axis = "runtime-timing-threshold"; + const left = baseline.match(/^var TIMING_CHECKS = ([^;]+);$/m)?.[1]; + const right = candidate.match(/^var TIMING_CHECKS = ([^;]+);$/m)?.[1]; + observed = left && right ? left === "false" && right !== "false" : null; + } else if (profile === "minify") { + axis = "runtime-minification"; + observed = Buffer.byteLength(candidate) < Buffer.byteLength(baseline); + } else { + axis = "serialized-bytecode"; + const left = bytecode(baseline); + const right = bytecode(candidate); + observed = left && right ? left !== right : null; + } + return { axis, observed }; +} diff --git a/scripts/qualify-jsconfuser-vm/fetch-encoder.mjs b/scripts/qualify-jsconfuser-vm/fetch-encoder.mjs new file mode 100644 index 00000000..935346d3 --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/fetch-encoder.mjs @@ -0,0 +1,17 @@ +// Clone the encoder at the qualifier's PIN into the ignored sandbox-tests/ +// workspace and install its runtime dependencies. Set JSCONFUSER_VM_ENCODER instead +// to qualify against an existing checkout of the same commit. Git and npm come +// from PATH. +import fs from "node:fs"; +import { execFileSync } from "node:child_process"; +import { ENCODER_REPOSITORY, PIN, encoderRoot } from "./audit.mjs"; + +if (process.env.JSCONFUSER_VM_ENCODER) + throw new Error("JSCONFUSER_VM_ENCODER already names an encoder checkout"); +if (fs.existsSync(encoderRoot)) + throw new Error(`${encoderRoot} exists; remove it to fetch again`); +const run = (command, args, cwd) => + execFileSync(command, args, { cwd, stdio: "inherit" }); +run("git", ["clone", "--no-checkout", ENCODER_REPOSITORY, encoderRoot]); +run("git", ["checkout", "--detach", PIN], encoderRoot); +run("npm", ["install", "--omit=dev", "--no-audit", "--no-fund"], encoderRoot); diff --git a/scripts/qualify-jsconfuser-vm/finalize.mjs b/scripts/qualify-jsconfuser-vm/finalize.mjs new file mode 100644 index 00000000..cc19eedc --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/finalize.mjs @@ -0,0 +1,186 @@ +import fs from "node:fs"; +import path from "node:path"; +import { exactEqual, parseRecord } from "./codec.mjs"; +import { emittedOptionEffect } from "./effects.mjs"; +import { manifest, sha256 } from "./audit.mjs"; + +// Large inspection/process records belong only in cell files. Summary consumers +// need selection, oracle dispositions and content identities, not copied IR. +export function cellSummary(record) { + const row = {}; + for (const key of [ + "id", + "case", + "profile", + "seed", + "options", + "revision", + "originEffectEligible", + "status", + "qualified", + "encoded", + "effectObservation", + "normalizedAgreement", + ]) + if (Object.hasOwn(record, key)) row[key] = record[key]; + return row; +} + +export function assertStoredDisposition(current, historical) { + if (!exactEqual(cellSummary(current), cellSummary(historical))) + throw new Error( + `Recovered disposition differs from original cell: ${current.id}`, + ); +} + +export function finalizeCells( + rows, + { readCell, readEncoded, writeCell, eligible }, +) { + const groups = new Map(); + for (const row of rows) { + const key = `${row.case}:${row.seed}`; + const group = groups.get(key) ?? []; + if (group.some((peer) => peer.profile === row.profile)) + throw new Error("Duplicate case/profile/seed cell"); + group.push(row); + groups.set(key, group); + } + const completed = new Map(); + for (const group of groups.values()) { + const baselineRow = group.find((row) => row.profile === "baseline"); + // Retain only the baseline projection, not its process stdout and analysis. + const baseline = baselineRow + ? readCell(baselineRow).inspection?.projection + : null; + const peers = new Map( + group.filter(eligible).map((row) => [row.profile, readEncoded(row)]), + ); + for (const row of group) { + const record = readCell(row); + if (!exactEqual(cellSummary(record), row)) + throw new Error(`Cell metadata changed: ${row.id}`); + record.effectObservation = emittedOptionEffect( + row.profile, + peers.get("baseline"), + peers.get(row.profile), + peers, + ); + record.normalizedAgreement = + record.inspection?.projection && baseline + ? exactEqual(record.inspection.projection, baseline) + : null; + record.qualified = + Boolean(record.qualified) && + record.normalizedAgreement === true && + (row.profile === "baseline" || + record.effectObservation.observed === true); + const references = writeCell(row, record); + completed.set(row.id, { ...cellSummary(record), files: references }); + } + } + return rows.map((row) => completed.get(row.id)); +} + +export function readCell(root, row, name = "result.json") { + return parseRecord(fs.readFileSync(path.join(root, row.id, name), "utf8")); +} + +export function cellFile(root, id, name) { + const file = path.join(root, id, name); + return { + path: `${id}/${name}`, + sha256: sha256(fs.readFileSync(file)), + bytes: fs.statSync(file).size, + }; +} + +const recoveryToolPaths = new Set([ + "scripts/qualify-jsconfuser-vm/qualify.mjs", + "test/scripts/qualify-jsconfuser-vm/qualify.test.js", + "scripts/qualify-jsconfuser-vm/finalize.mjs", +]); +// `changed` lists the qualifier tools that differ between the original run's +// commit and the current worktree (`changedTools` in the audit module). +export function assertRecoveryBoundary( + original, + preparation, + current, + rawManifestBytes, + changed, +) { + if ( + preparation.originalExitCode !== 134 || + preparation.originalPublication !== "absent" || + preparation.identical !== true || + !exactEqual(original, preparation.before) || + !exactEqual(original, preparation.current) || + sha256(rawManifestBytes) !== preparation.rawManifestSha256 + ) + throw new Error( + "Recovery preparation does not identify the original aborted run", + ); + const withoutTools = (input) => { + const boundary = { ...input }; + delete boundary.tools; + // A recovery tool commit also moves the decoder commit; the src tree in the + // boundary is what must stay unchanged. + delete boundary.decoderCommit; + return boundary; + }; + if (!exactEqual(withoutTools(original), withoutTools(current))) + throw new Error("Recovery source/runtime/dependency boundary changed"); + // Only a clean original worktree leaves Git identifying the original tools. + if (original.tools?.status !== "") + throw new Error("Recovery needs the original tools committed"); + if (changed.some((file) => !recoveryToolPaths.has(file))) + throw new Error("Recovery changed an oracle or unrelated tool"); +} + +export function verifyRawPopulation(root, expected) { + if (!exactEqual(expected, manifest(root))) + throw new Error("Frozen raw population changed"); +} + +export function recoveryRows(root, population, expected) { + const members = new Map(expected.map((member) => [member.path, member])); + const rows = []; + for (const caseName of Object.keys(population.sources)) + for (const profile of population.profiles) + for (const seed of population.seeds) { + const id = `${caseName}--${profile.name}--${seed}`; + for (const name of ["source.js", "result.json", "qualification.json"]) + if (!members.has(`${id}/${name}`)) + throw new Error(`Incomplete raw cell ${id}/${name}`); + const result = readCell(root, { id }); + if ( + result.id !== id || + result.case !== caseName || + result.profile !== profile.name || + result.seed !== seed || + !exactEqual(result.options, profile.options) || + fs.readFileSync(path.join(root, id, "source.js"), "utf8") !== + population.sources[caseName] + ) + throw new Error(`Raw cell selection/input mismatch: ${id}`); + if (result.encoded && !members.has(`${id}/encoded.js`)) + throw new Error(`Raw encoded identity mismatch: ${id}`); + if ( + result.encoded && + (!members.has(`${id}/raw-encoded.js`) || + !members.has(`${id}/generation-journal.ndjson`) || + fs.readFileSync(path.join(root, id, "raw-encoded.js"), "utf8") !== + (result.generation?.raw ?? "")) + ) + throw new Error(`Raw generation provenance incomplete: ${id}`); + rows.push(cellSummary(result)); + } + const actualResults = expected.filter( + (member) => + member.path.endsWith("/result.json") && + !member.path.startsWith("supporting-inputs/"), + ); + if (actualResults.length !== rows.length) + throw new Error("Raw cell census mismatch"); + return rows; +} diff --git a/scripts/qualify-jsconfuser-vm/generation.mjs b/scripts/qualify-jsconfuser-vm/generation.mjs new file mode 100644 index 00000000..98041bfd --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/generation.mjs @@ -0,0 +1,135 @@ +import crypto from "node:crypto"; +const hash = (value) => crypto.createHash("sha256").update(value).digest("hex"); +export async function generationSession( + request, + { + loadCompiler, + loadEncoder, + emit, + setTimer = setTimeout, + clearTimer = clearTimeout, + }, +) { + let state = request.seed >>> 0, + draws = 0, + Compiler, + originalRun; + const originalRandom = Math.random, + savedPath = process.env.PATH, + processes = []; + const rng = () => ({ + algorithm: "mulberry32", + seed: request.seed, + draws, + state, + }); + function random() { + draws++; + state = (state + 0x6d2b79f5) >>> 0; + let value = Math.imul(state ^ (state >>> 15), state | 1); + value ^= value + Math.imul(value ^ (value >>> 7), value | 61); + emit({ event: "rng-draw", rng: rng() }); + return ((value ^ (value >>> 14)) >>> 0) / 0x100000000; + } + emit({ + event: "generation-start", + identities: request.identities, + rng: rng(), + }); + try { + process.env.PATH = `${request.javaRoot}/bin:${savedPath ?? ""}`; + Compiler = await loadCompiler(); + originalRun = Compiler.prototype.run; + Compiler.prototype.run = function (callback) { + const record = { + id: processes.length, + executable: this.javaPath, + argv: [...this.commandArguments], + startedAt: new Date().toISOString(), + timeoutMs: 60000, + stdout: "", + stderr: "", + exitCode: null, + signal: null, + timedOut: false, + }; + processes.push(record); + let timer, + child, + completed = false; + emit({ + event: "closure-attempt", + process: { ...record, argv: [...this.commandArguments] }, + rng: rng(), + }); + const finish = (exitCode, signal) => { + if (completed) return; + completed = true; + clearTimer(timer); + Object.assign(record, { + exitCode, + signal, + completedAt: new Date().toISOString(), + stdoutSha256: hash(record.stdout), + }); + emit({ event: "closure-finish", process: { ...record }, rng: rng() }); + }; + child = originalRun.call(this, (exitCode, stdout, stderr) => { + record.stdout = stdout; + record.stderr = stderr; + callback(exitCode, stdout, stderr); + }); + record.argv = [...this.commandArguments]; + record.pid = child.pid; + emit({ event: "closure-start", process: { ...record }, rng: rng() }); + child.stdout?.on("data", (chunk) => { + record.stdout += String(chunk); + emit({ event: "closure-stdout", id: record.id, chunk: String(chunk) }); + }); + child.stderr?.on("data", (chunk) => { + record.stderr += String(chunk); + emit({ event: "closure-stderr", id: record.id, chunk: String(chunk) }); + }); + child.once("error", (error) => { + record.error = { name: error.name, message: error.message }; + emit({ event: "closure-error", process: { ...record }, rng: rng() }); + }); + child.once("close", (code, signal) => finish(code, signal)); + timer = setTimer(() => { + record.timedOut = true; + emit({ event: "closure-watchdog", process: { ...record }, rng: rng() }); + child.kill("SIGKILL"); + }, 60000); + return child; + }; + const encoder = await loadEncoder(); + Math.random = random; + const result = await encoder.obfuscate(request.source, request.options); + // Closure close and callback are delivered in the same event turn by the + // pinned wrapper. Keep the finish event before final success publication. + await new Promise((resolve) => setTimeout(resolve, 0)); + if ( + request.options.minify && + (processes.length !== 1 || + processes[0].exitCode !== 0 || + processes[0].timedOut) + ) + throw new Error("Missing unique successful Closure process"); + const resultRecord = { raw: result.code, rng: rng(), processes }; + emit({ event: "generation-success", rng: rng(), processes }); + return resultRecord; + } catch (error) { + error.provenance = { rng: rng(), processes }; + emit({ + event: "generation-error", + error: { name: error.name, message: error.message }, + ...error.provenance, + }); + throw error; + } finally { + Math.random = originalRandom; + if (Compiler && originalRun) Compiler.prototype.run = originalRun; + if (savedPath === undefined) delete process.env.PATH; + else process.env.PATH = savedPath; + } +} diff --git a/scripts/qualify-jsconfuser-vm/layout.mjs b/scripts/qualify-jsconfuser-vm/layout.mjs new file mode 100644 index 00000000..bc26400c --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/layout.mjs @@ -0,0 +1,421 @@ +import { parse } from "../../node_modules/@babel/parser/lib/index.js"; +import { + makeContext, + isWordArray, + numberValue, + propertyName, + referenceCount, + topLevelDeclarations, +} from "../../src/vm/jsconfuser-vm/container-role-helpers.js"; +import { + BASE64_SHA256, + runtimeSignature, +} from "../../src/vm/jsconfuser-vm/runtime-source-signature.js"; +import { exactEqual } from "./codec.mjs"; + +// The literal values of a constant-pool array, with minify's `!0`, `!1` and `void 0` spellings; +// null when an element is not a literal. +function poolValues(node, context) { + if (node?.type !== "ArrayExpression") return null; + const values = []; + for (const element of node.elements) { + if ( + element?.type === "StringLiteral" || + element?.type === "NumericLiteral" || + element?.type === "BooleanLiteral" + ) + values.push(element.value); + else if (element?.type === "NullLiteral") values.push(null); + else if ( + element?.type === "UnaryExpression" && + element.operator === "!" && + (numberValue(element.argument) === 0 || + numberValue(element.argument) === 1) + ) + values.push(numberValue(element.argument) === 0); + else if ( + (element?.type === "Identifier" && + element.name === "undefined" && + !context.bindingFor(element)) || + (element?.type === "UnaryExpression" && + element.operator === "void" && + numberValue(element.argument) === 0) + ) + values.push(undefined); + else return null; + } + return values; +} + +const NUMERIC_TRANSPORT_SIGNATURES = new Set([ + "5e639974414e47e7fb566a6ae57cd56b9a6329a776218898b5976f91d01cb19c", + "61933608ccc41584c75e9d3462d41856959327805f6ee981229e9bf6beb8fdd4", +]); +class LayoutDecline extends Error { + constructor(code, message) { + super(message); + this.code = code; + } +} +const transportCheck = (condition, code, message) => { + if (!condition) throw new LayoutDecline(code, message); +}; +function numericTransport(context, call, resolve) { + transportCheck( + call?.type === "CallExpression" && call.arguments.length === 1, + "numeric-transport-call", + "Numeric transport requires one exact payload argument", + ); + const binding = context.bindingFor(call.callee), + fn = binding?.path.node; + transportCheck( + binding?.constant && + fn?.type === "FunctionDeclaration" && + context.paths.get(fn).parentPath.isProgram() && + referenceCount(context, binding) === 1, + "numeric-transport-binding", + "Numeric transport decoder must be stable, top-level and used once", + ); + transportCheck( + NUMERIC_TRANSPORT_SIGNATURES.has( + runtimeSignature({ params: fn.params, body: fn.body }, context), + ), + "numeric-transport-body", + "Numeric transport body differs from the pinned complete decoder", + ); + const mode = fn.body.body[0].test.argument; + if (mode.type === "Identifier") { + const flag = context.bindingFor(mode); + transportCheck( + flag?.constant && + context.paths.get(flag.path.node).parentPath.parentPath.isProgram() && + flag.path.node.init?.type === "BooleanLiteral" && + flag.path.node.init.value === false && + referenceCount(context, flag) === 1, + "numeric-transport-mode", + "Numeric transport mode must be the stable disabled binding", + ); + } else + transportCheck( + mode.type === "BooleanLiteral" && mode.value === false, + "numeric-transport-mode", + "Numeric transport mode must be the disabled literal", + ); + const declarations = topLevelDeclarations(context); + const bytes = fn.body.body[1]?.declarations?.[0]?.init; + const helper = context.bindingFor(bytes?.callee); + transportCheck( + helper?.constant && + declarations.some( + (entry) => entry.binding === helper && entry.kind === "function", + ) && + runtimeSignature(helper.path.node, context) === BASE64_SHA256 && + context.sameRef(bytes.arguments[0], context.bindingFor(fn.params[0])), + "numeric-transport-helper", + "Numeric transport byte helper/body/parameter connection changed", + ); + const payload = resolve(call.arguments[0], false); + const payloadBinding = context.bindingFor(call.arguments[0]); + transportCheck( + isWordArray(payload) && + (!payloadBinding || referenceCount(context, payloadBinding) === 1), + "numeric-transport-payload", + "Numeric transport payload must be a stable numeric word array used only by the decoder call", + ); + return payload; +} + +// Observe emitted layout independently of public admission. Payload constructor +// binding and receiver-connected optional slot writes anchor the census. +function observeRuntimeLayout(source) { + try { + const context = makeContext(parse(source, { sourceType: "script" })); + const resolve = (node, transport = true) => { + if (node?.type === "Identifier") { + const binding = context.bindingFor(node); + if ( + !binding?.constant || + !context.paths + .get(binding.path.node) + .parentPath.parentPath.isProgram() + ) + return null; + node = binding.path.node.init; + } + return transport && node?.type === "CallExpression" + ? numericTransport(context, node, resolve) + : node; + }; + const candidates = context + .nodes(context.program) + .filter(({ node }) => node.type === "NewExpression") + .map(({ node }) => { + const fn = context.bindingFor(node.callee)?.path.node; + if (fn?.type !== "FunctionDeclaration") return null; + const args = node.arguments.map((argument) => resolve(argument)); + const words = args.findIndex((arg) => isWordArray(arg)); + const pool = args.findIndex((arg) => + poolValues(arg, context)?.some((value) => typeof value === "string"), + ); + if (words < 0 || pool < 0 || words === pool) return null; + const assignments = context + .nodes(fn) + .map(({ node }) => node) + .filter( + (node) => + node.type === "AssignmentExpression" && + node.operator === "=" && + node.left.type === "MemberExpression" && + !node.left.computed && + node.left.object.type === "ThisExpression", + ); + const fieldAt = (position) => + assignments + .filter((item) => + context.sameRef( + item.right, + context.bindingFor(fn.params[position]), + ), + ) + .map((item) => propertyName(item.left.property)); + const wordFields = fieldAt(words), + poolFields = fieldAt(pool); + const regFields = assignments + .filter( + (item) => + item.right.type === "ArrayExpression" && + !item.right.elements.length, + ) + .map((item) => propertyName(item.left.property)); + if ( + wordFields.length !== 1 || + poolFields.length !== 1 || + regFields.length !== 1 + ) + return null; + return { + node, + constructor: context.bindingFor(node.callee), + words, + pool, + wordField: wordFields[0], + regField: regFields[0], + args: args.length, + }; + }) + .filter(Boolean); + if (candidates.length !== 1) return null; + const vm = candidates[0]; + const functions = context + .nodes(context.program) + .filter(({ node }) => + ["FunctionDeclaration", "FunctionExpression"].includes(node.type), + ); + const entryPath = context.paths.get(vm.node); + const holder = entryPath.parentPath.node; + const instance = context.bindingFor( + holder.type === "VariableDeclarator" + ? holder.id + : holder.type === "AssignmentExpression" + ? holder.left + : null, + ); + const actualVm = (node) => + node === vm.node || (instance && context.sameRef(node, instance)); + const receiverMatches = (node, receiver) => + receiver === "this" + ? node?.type === "ThisExpression" + : context.sameRef(node, receiver); + const calleeFunction = (call) => { + if (call.callee.type === "Identifier") { + const binding = context.bindingFor(call.callee); + return functions.find(({ node }) => node === binding?.path.node)?.node; + } + if (call.callee.type !== "MemberExpression" || call.callee.computed) + return null; + return functions.find(({ path }) => { + const left = path.parentPath.node.left; + return ( + left?.type === "MemberExpression" && + !left.computed && + propertyName(left.property) === propertyName(call.callee.property) && + left.object?.type === "MemberExpression" && + propertyName(left.object.property) === "prototype" && + context.sameRef(left.object.object, vm.constructor) + ); + })?.node; + }; + const roots = context + .nodes(context.program) + .filter(({ node }) => node.type === "CallExpression") + .map(({ node }) => { + const index = node.arguments.findIndex(actualVm), + method = + node.callee.type === "MemberExpression" && + actualVm(node.callee.object); + const fn = index >= 0 || method ? calleeFunction(node) : null; + return fn + ? { + node: fn, + receiver: method ? "this" : context.bindingFor(fn.params[index]), + } + : null; + }) + .filter(Boolean) + .filter(({ node, receiver }) => + context + .nodes(node) + .some( + ({ node }) => + node.type === "MemberExpression" && + !node.computed && + propertyName(node.property) === vm.wordField && + receiverMatches(node.object, receiver), + ), + ); + if (roots.length !== 1) return null; + const connected = [...roots]; + for (const entry of connected) + for (const { node, path } of context.nodes(entry.node)) { + if ( + node.type !== "CallExpression" || + path.getFunctionParent()?.node !== entry.node + ) + continue; + const index = node.arguments.findIndex((arg) => + receiverMatches(arg, entry.receiver), + ); + const method = + node.callee.type === "MemberExpression" && + receiverMatches(node.callee.object, entry.receiver); + const fn = index >= 0 || method ? calleeFunction(node) : null; + if (fn && !connected.some((entry) => entry.node === fn)) + connected.push({ + node: fn, + receiver: method ? "this" : context.bindingFor(fn.params[index]), + }); + } + const guardKeys = []; + for (const { node: fn, receiver } of connected) + for (const { node, path } of context.nodes(fn)) { + if (path.getFunctionParent()?.node !== fn) continue; + if (node.type !== "LogicalExpression" || node.operator !== "&&") + continue; + const test = node.left; + if ( + test.type !== "BinaryExpression" || + test.operator !== "===" || + test.right.type !== "StringLiteral" || + test.right.value !== "number" || + test.left.type !== "UnaryExpression" || + test.left.operator !== "typeof" + ) + continue; + const member = test.left.argument; + if (member.type !== "MemberExpression" || member.computed) continue; + const binding = context.bindingFor(member.object), + object = binding?.path.node.init; + const key = propertyName(member.property); + if ( + !binding?.constant || + object?.type !== "ObjectExpression" || + object.properties.some((item) => propertyName(item.key) === key) + ) + continue; + // The guarded update must use a register array derived from this VM's + // register field in the same function, not a similarly named other array. + const registers = context + .nodes(fn) + .map(({ node }) => node) + .filter((item) => + ["AssignmentExpression", "VariableDeclarator"].includes(item.type), + ) + .filter((item) => { + const rhs = item.right ?? item.init; + return ( + rhs?.type === "MemberExpression" && + !rhs.computed && + propertyName(rhs.property) === vm.regField && + receiverMatches(rhs.object, receiver) + ); + }) + .map((item) => context.bindingFor(item.left ?? item.id)) + .filter(Boolean); + if ( + !context + .nodes(node.right) + .some( + ({ node: item }) => + item.type === "MemberExpression" && + item.computed && + registers.some((reg) => context.sameRef(item.object, reg)), + ) + ) + continue; + guardKeys.push(key); + } + return { + words: vm.words, + pool: vm.pool, + argumentCount: vm.args, + optionalSlotGuards: guardKeys.length, + }; + } catch (error) { + if (error instanceof LayoutDecline) throw error; + return null; + } +} + +export function diagnoseRuntimeLayout(source) { + try { + const result = observeRuntimeLayout(source); + return result + ? { ok: true, result, diagnostic: null } + : { + ok: false, + result: null, + diagnostic: { + code: "unconnected-layout", + message: "No unique binding-connected runtime layout", + }, + }; + } catch (error) { + if (!(error instanceof LayoutDecline)) throw error; + return { + ok: false, + result: null, + diagnostic: { code: error.code, message: error.message }, + }; + } +} +export function runtimeLayout(source) { + return diagnoseRuntimeLayout(source).result; +} + +export function classLayoutEffect(control, candidate) { + const left = runtimeLayout(control), + right = runtimeLayout(candidate); + if (!left || !right) + return { + observed: null, + witness: "unconnected-layout", + control: left, + candidate: right, + }; + const erased = left.optionalSlotGuards > 0 && right.optionalSlotGuards === 0; + const permuted = + left.words !== right.words || + left.pool !== right.pool || + left.argumentCount !== right.argumentCount; + return { + observed: + erased || permuted ? true : exactEqual(left, right) ? false : null, + witness: erased + ? "connected-optional-slots-erased" + : permuted + ? "payload-role-permutation" + : "no-observed-class-witness", + control: left, + candidate: right, + }; +} diff --git a/scripts/qualify-jsconfuser-vm/oracles.mjs b/scripts/qualify-jsconfuser-vm/oracles.mjs new file mode 100644 index 00000000..fb4a7207 --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/oracles.mjs @@ -0,0 +1,336 @@ +import { parse } from "../../node_modules/@babel/parser/lib/index.js"; +import traverse from "../../node_modules/@babel/traverse/lib/index.js"; +import generate from "../../node_modules/@babel/generator/lib/index.js"; +import * as t from "../../node_modules/@babel/types/lib/index.js"; +import { diagnoseContainer } from "../../src/vm/jsconfuser-vm/extract-container.js"; +import { diagnoseWordcode } from "../../src/vm/jsconfuser-vm/read-wordcode.js"; +import { diagnoseReferences } from "../../src/vm/jsconfuser-vm/validate-references.js"; +import { diagnoseStandaloneInput } from "../../src/vm/jsconfuser-vm/diagnose-standalone.js"; +import { decodeJsconfuserVm } from "../../src/plugin/jsconfuser-vm.js"; +import { exactEqual } from "./codec.mjs"; +import { runtimeLayout } from "./layout.mjs"; + +export const PROJECTION_VERSION = "vm-qualification-normalized.v1"; +export const SEMANTIC_LIMITS = { + EH: "E models CALL/CALL_METHOD/NEW and explicit THROW exceptions; property/coercion exception edges remain incomplete; H projects E routes.", + F: "Direct callees differ from defective fixed-r2/r3 result provenance; returned closure facts are limited.", + G: "Static event/fallback labels; method-receiver destination, external-result/NEW precision and inherited F result provenance remain unqualified; imprecise joined arguments can suppress events.", +}; +function state(ast) { + const nodes = new Set(), + aliases = []; + const visit = (node) => { + if (!node?.type) return; + if (nodes.has(node)) { + aliases.push(node.type); + return; + } + nodes.add(node); + for (const key of t.VISITOR_KEYS[node.type] ?? []) { + const child = node[key]; + if (Array.isArray(child)) child.forEach(visit); + else visit(child); + } + }; + visit(ast); + const bindings = [], + detached = []; + traverse(ast, { + Scopable(path) { + for (const name of Object.keys(path.scope.bindings).sort()) { + const binding = path.scope.bindings[name]; + bindings.push({ + name, + references: binding.references, + constant: binding.constant, + violations: binding.constantViolations.length, + }); + for (const ref of binding.referencePaths) + if (!nodes.has(ref.node)) detached.push(name); + } + }, + }); + return { bindings, aliases, detached }; +} +export function outputChecks(output, input, result) { + try { + const ast = parse(output, { sourceType: "script" }); + const first = state(ast), + second = state(parse(output, { sourceType: "script" })); + const findings = []; + const inputAst = parse(input, { sourceType: "script" }); + const admitted = diagnoseContainer(inputAst); + const sourceRoles = admitted.ok + ? { + words: admitted.result.roles.words.values, + constructors: admitted.result.constructors, + methods: admitted.result.methods, + } + : null; + const recoveredFunctions = []; + // A numeric stream read inside a loop-connected switch is a dispatcher, + // regardless of binding names. An ordinary user switch does not read its + // discriminant through an advancing stream cursor. + traverse(ast, { + FunctionDeclaration(path) { + if (/^__recovered_function_[0-9]+$/.test(path.node.id?.name)) + recoveredFunctions.push(path.node.id.name); + }, + ArrayExpression(path) { + if ( + sourceRoles && + exactEqual( + path.node.elements.map((node) => + node?.type === "NumericLiteral" ? node.value : null, + ), + sourceRoles.words, + ) + ) + findings.push("retained-source-wordcode"); + }, + SwitchStatement(path) { + const selector = path.node.discriminant; + const loop = path.findParent((parent) => parent.isLoop()); + const advancingStream = (read) => { + if (read?.type !== "MemberExpression" || !read.computed) return false; + if (read.property.type === "UpdateExpression") return true; + const cursor = + read.property.type === "Identifier" && + path.scope.getBinding(read.property.name); + return Boolean( + cursor && + cursor.constantViolations.some( + (write) => + write.isUpdateExpression() && + write.findParent((parent) => parent.node === loop?.node), + ), + ); + }; + const selectedBinding = + selector.type === "Identifier" && + path.scope.getBinding(selector.name); + const selectedReads = selectedBinding + ? [ + selectedBinding.path.node.init, + ...selectedBinding.constantViolations.map( + (write) => write.node.right, + ), + ] + : []; + if ( + loop && + (advancingStream(selector) || selectedReads.some(advancingStream)) + ) + findings.push("numeric-stream-dispatch"); + if ( + selector.type === "Identifier" && + path.findParent((parent) => parent.isLoop()) + ) { + const binding = path.scope.getBinding(selector.name); + if ( + binding && + binding.constantViolations.some( + (write) => + write.findParent((parent) => parent.node === path.node) && + write.node.right?.type === "NumericLiteral", + ) + ) + findings.push("opaque-state-dispatch"); + } + }, + }); + const forbidden = new Set([ + "BYTECODE", + "CONSTANTS", + "ENCODE_BYTECODE", + "VM", + "Closure", + "Upvalue", + "decodeBytecode", + "__recovered_pc", + "__recovered_dispatch", + ]); + traverse(ast, { + Identifier(path) { + if (forbidden.has(path.node.name)) findings.push(path.node.name); + }, + CallExpression(path) { + if ( + path.node.callee.type === "Identifier" && + ["eval", "Function"].includes(path.node.callee.name) + ) + findings.push(path.node.callee.name); + }, + NewExpression(path) { + if ( + path.node.callee.type === "Identifier" && + path.node.callee.name === "Function" + ) + findings.push("Function"); + }, + }); + if (runtimeLayout(output)) findings.push("connected-VM-payload"); + const fresh = + output !== input && generate(ast).code !== generate(inputAst).code; + const outputIdentity = result?.output === output; + const summary = result?.simplification?.summary; + const recoveredCoverage = + result?.coverage?.instructions === result?.instructionCount && + result?.coverage?.functions === result?.functionCount && + summary && + summary.emitted + summary.safelyEliminated + summary.provenUnreachable === + result.instructionCount && + result?.statements?.length === result.functionCount && + recoveredFunctions.length === result.functionCount; + const proof = result?.proof; + const complete = + result?.schemaVersion === "jsconfuser-vm-standalone.v2" && + proof?.allPredecessorsReconstructed === true && + proof?.freshSimplificationState === true && + proof?.simplificationDispositionsDisjoint === true && + proof?.finalParseValidated === true && + proof?.finalResidueValidated === true; + const derivedState = + !first.aliases.length && + !first.detached.length && + exactEqual(first, second); + return { + ok: + fresh && + outputIdentity && + recoveredCoverage && + complete && + derivedState && + !findings.length, + fresh, + outputIdentity, + recoveredCoverage: Boolean(recoveredCoverage), + sourceRoles, + complete, + parse: true, + derivedState, + stateBoundary: "fresh-output-parse", + residue: [...new Set(findings)], + }; + } catch (error) { + return { + ok: false, + parse: false, + error: { name: error.name, message: error.message }, + }; + } +} +export function inspectCurrent(source, adapter = decodeJsconfuserVm) { + const ast = parse(source, { sourceType: "script" }); + const before = state(ast), + serialized = JSON.stringify(ast); + const container = diagnoseContainer(ast), + after = state(ast); + const inputState = + exactEqual(before, after) && + serialized === JSON.stringify(ast) && + !after.detached.length && + !after.aliases.length; + const wordcode = container.ok ? diagnoseWordcode(container.result) : null; + const references = wordcode?.ok + ? diagnoseReferences(container.result, wordcode.result) + : null; + const preflight = diagnoseStandaloneInput(source); + const publicRecord = adapter(source); + const checks = publicRecord.ok + ? outputChecks(publicRecord.output, source, publicRecord.result) + : { ok: false, declined: true }; + const model = preflight.ok ? preflight.model : null; + const projection = model + ? { + version: PROJECTION_VERSION, + words: model.wordcode.words, + constants: model.constants, + root: container.result?.roles.scalars.MAIN_START_PC.value, + registers: container.result?.roles.scalars.MAIN_REG_COUNT.value, + frame: container.result + ? { + header: container.result.roles.scalars.HEADER_SIZE.value, + start: container.result.roles.scalars.FRAME_START.value, + slots: container.result.roles.slots.values, + } + : null, + instructions: model.wordcode.instructions, + functions: model.functions, + } + : null; + const firstBoundary = !container.ok + ? { refusedBy: "extract-container", diagnostic: container.diagnostic } + : wordcode && !wordcode.ok + ? { refusedBy: "read-wordcode", diagnostic: wordcode.diagnostic } + : references && !references.ok + ? { + refusedBy: "validate-references", + diagnostic: references.diagnostic, + } + : !preflight.ok + ? { + refusedBy: "standalone-diagnosis", + diagnostic: preflight.diagnostic, + } + : !publicRecord.ok + ? { + refusedBy: "standalone-decode", + diagnostic: publicRecord.diagnostic, + } + : null; + return { + publicRecord, + container, + wordcode, + references, + analysis: preflight, + projection, + checks, + inputState, + firstBoundary, + semanticLimits: SEMANTIC_LIMITS, + }; +} + +// Historical output is deliberately absent from the execution request. The +// injected observation seam exists for oracle tests, not production decoding. +export async function assessCurrent(inspected, source, expected, observe) { + if (!inspected.publicRecord.ok) + return { qualified: false, status: "declined", behavior: null }; + if (!observe) + return { + qualified: false, + status: "parser-only", + behavior: { qualified: false }, + }; + const sourceObservation = + expected.sourceObservation ?? (await observe(source)), + encodedObservation = + expected.encodedObservation ?? (await observe(expected.encoded)), + decodedObservation = await observe(inspected.publicRecord.output); + const behavior = { + sourceObservation, + encodedObservation, + decodedObservation, + }; + const values = [sourceObservation, encodedObservation, decodedObservation]; + const equal = + values.every((record) => record.kind === "value") && + exactEqual(sourceObservation, encodedObservation) && + exactEqual(sourceObservation, decodedObservation); + const independent = + !Object.hasOwn(expected, "value") || + exactEqual(sourceObservation.value, expected.value); + return { + qualified: + equal && independent && inspected.checks.ok && inspected.inputState, + status: + !equal || !independent + ? "behavior-mismatch" + : inspected.checks.ok && inspected.inputState + ? "accepted" + : "oracle-failed", + behavior, + }; +} diff --git a/scripts/qualify-jsconfuser-vm/patch.mjs b/scripts/qualify-jsconfuser-vm/patch.mjs new file mode 100644 index 00000000..3b9778f9 --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/patch.mjs @@ -0,0 +1,129 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { sources, baselineOptions } from './config.mjs'; +import { runWorker } from './qualify.mjs'; +import { snapshot, PIN } from './audit.mjs'; +import { stringifyRecord, exactEqual } from './codec.mjs'; +import { diagnoseContainer } from '../../src/vm/jsconfuser-vm/extract-container.js'; +import { diagnosePatchAddedOpcodes } from '../../src/vm/jsconfuser-vm/observe-patch-added-opcodes.js'; +import { diagnoseSelfModifyingRegions } from '../../src/vm/jsconfuser-vm/observe-self-modifying.js'; +import { diagnoseNormalizeSelfModifyingWordcode } from '../../src/vm/jsconfuser-vm/normalize-self-modifying-wordcode.js'; + +// Fixed pre-generation protocol: all subsets, known sources, fresh mulberry32 +// seeds, same-case/seed removal peers, native equality AND current recovery, +// residue and consumed state. Named damage controls are in the focused tests. +// Worker limits: generation 65s, inspection (with the census) 20s, behavior 12s, native VM 10s. +// Cells run PATCH_JOBS at a time (default 4: each worker uses about 1.4 cores, and more jobs only +// slowed every worker); records, summary and stdout keep the fixed case/seed/profile order. +export const patchOptions = ['selfModifying', 'aliasedOpcodes', 'macroOpcodes', 'specializedOpcodes', 'antiInstrumentation']; +export const patchCases = ['arithmetic', 'closures', 'tryFinally', 'patchArrays']; +export const patchSeeds = [211, 212, 213]; +export const patchProfiles = Array.from({ length: 1 << patchOptions.length }, (_, mask) => ({ + name: mask === 0 ? 'baseline' : patchOptions.filter((_, bit) => mask & (1 << bit)).join('+'), + options: Object.fromEntries(patchOptions.map((option, bit) => [option, Boolean(mask & (1 << bit))])), +})); +const kindByOption = { aliasedOpcodes: 'ALIAS', macroOpcodes: 'MACRO', specializedOpcodes: 'SPECIALIZED', antiInstrumentation: 'SYNTHETIC' }; +export function patchAddedOpcodeCensus(source) { + const container = diagnoseContainer(source); + if (!container.ok) return { ok: false, diagnostic: container.diagnostic }; + const addedOpcodes = diagnosePatchAddedOpcodes(source); + if (!addedOpcodes.ok) return { ok: false, diagnostic: addedOpcodes.diagnostic }; + const observed = diagnoseSelfModifyingRegions(container.result, addedOpcodes.result, { requirePatch: false }); + if (!observed.ok) return { ok: false, diagnostic: observed.diagnostic }; + const counts = Object.fromEntries(patchOptions.map(option => [option, option === 'selfModifying' + ? observed.result.patches.length : observed.result.instructions.filter(instruction => instruction.name === kindByOption[option]).length])); + return { ok: true, counts, emitted: { + aliasedOpcodes: addedOpcodes.result.alias?.aliases.length ?? 0, + macroOpcodes: addedOpcodes.result.macro?.macros.length ?? 0, + specializedOpcodes: addedOpcodes.result.specialized?.specialized.length ?? 0, + antiInstrumentation: addedOpcodes.result.anti?.syntheticCases.length ?? 0, + }, patchedKinds: [...new Set(observed.result.patches.flatMap(patch => patch.instructions.map(instruction => instruction.name)))].sort() }; +} +// The census and the PATCH normalization run in the inspection worker, so the scheduling +// process stays free. +export function patchCensusOf(source, selfModifying) { + const census = patchAddedOpcodeCensus(source); + const container = diagnoseContainer(source); + const normalization = selfModifying && container.ok + ? diagnoseNormalizeSelfModifyingWordcode(container.result, { source }) : null; + return { census, normalizationDiagnostic: normalization?.diagnostic }; +} +export function assertNamedPatchDiagnostic(diagnosis, code) { + if (diagnosis.ok || diagnosis.diagnostic?.code !== code) + throw new Error(`Expected PATCH mechanism ${code}; observed ${diagnosis.diagnostic?.code ?? 'success'}`); +} +export async function main(args = process.argv.slice(2)) { + const output = args[0]; + const seeds = args[1] ? args[1].split(',').map(Number) : patchSeeds; + const cases = args[2] ? args[2].split(',') : patchCases; + if (!output || seeds.some(seed => !patchSeeds.includes(seed)) || cases.some(name => !patchCases.includes(name))) + throw new Error('Use patch.mjs OUTPUT [211,212,213] [arithmetic,closures,tryFinally,patchArrays]'); + if (fs.existsSync(output)) throw new Error('Output directory must not exist'); + const before = snapshot({ generation: true }); + fs.mkdirSync(output, { recursive: true }); + const protocol = { pin: PIN, seeds, cases, profiles: patchProfiles, limitsMs: { generation: 65000, inspection: 20000, behavior: 12000, native: 10000 }, inputs: before }; + fs.writeFileSync(path.join(output, 'protocol.json'), stringifyRecord(protocol)); + const records = []; + const worker = async (request, limit) => { + const result = await runWorker(request, limit); + if (!result.ok || result.process.exitCode !== 0 || result.process.timedOut || result.process.signal) + throw new Error(`Qualification worker failed: ${JSON.stringify(result.error)}`); + return result; + }; + const jobs = Math.max(1, Number(process.env.PATCH_JOBS) || 4); + const pool = async (items, run) => { + const results = new Array(items.length); + let next = 0; + await Promise.all(Array.from({ length: Math.min(jobs, items.length) }, async () => { + while (next < items.length) { const index = next++; results[index] = await run(items[index]); } + })); + return results; + }; + const groups = cases.flatMap(caseName => seeds.map(seed => ({ caseName, seed, source: sources[caseName] }))); + const sourceBehaviors = await pool(groups, group => worker({ action: 'behavior', source: group.source }, 12000)); + const cell = async ({ group, sourceBehavior, profile }) => { + const { caseName, seed, source } = group; + const name = `${caseName}--${profile.name}--${seed}`; + const directory = path.join(output, name); fs.mkdirSync(directory); + fs.writeFileSync(path.join(directory, 'source.js'), source); + const journalPath = path.join(directory, 'generation.ndjson'); fs.writeFileSync(journalPath, ''); + const generation = await worker({ action: 'generate', source, options: { ...baselineOptions, ...profile.options }, seed, journalPath, identities: { java: before.java, jar: before.jar } }, 65000); + const encoded = generation.result.raw.replace(/^(?:\/\/[^\n]*\r?\n)+/, ''); + fs.writeFileSync(path.join(directory, 'encoded.js'), encoded); + const encodedBehavior = await worker({ action: 'behavior', source: encoded }, 12000); + const inspected = await worker({ action: 'patch-inspect', source: encoded, selfModifying: profile.options.selfModifying }, 20000); + const current = inspected.result.inspection; + const behavior = exactEqual(sourceBehavior.result, encodedBehavior.result) && sourceBehavior.result.kind === 'value'; + const { census, normalizationDiagnostic } = inspected.result; + let decodedBehavior = null; + if (current.publicRecord.ok) { + fs.writeFileSync(path.join(directory, 'decoded.js'), current.publicRecord.output); + decodedBehavior = await worker({ action: 'behavior', source: current.publicRecord.output }, 12000); + } + const recoveryQualified = behavior && current.publicRecord.ok && current.checks.ok && current.inputState + && exactEqual(sourceBehavior.result, decodedBehavior?.result); + const record = { name, caseName, seed, profile, census, recoveryQualified, behavior, publicDiagnostic: current.publicRecord.diagnostic, normalizationDiagnostic, checks: current.checks, + processes: [sourceBehavior.process, generation.process, encodedBehavior.process, inspected.process, ...(decodedBehavior ? [decodedBehavior.process] : [])] }; + fs.writeFileSync(path.join(directory, 'record.json'), stringifyRecord(record)); + const { processes, ...row } = record; + return { row, line: `${name}: ${recoveryQualified ? 'recovered' : 'finding'} ${normalizationDiagnostic?.code ?? current.publicRecord.diagnostic?.code ?? ''}\n` }; + }; + const cells = groups.flatMap((group, index) => patchProfiles.map(profile => ({ group, sourceBehavior: sourceBehaviors[index], profile }))); + const done = await pool(cells, cell); + for (const { row, line } of done) { records.push(row); process.stdout.write(line); } + for (const [index] of groups.entries()) { + const peers = records.slice(index * patchProfiles.length, (index + 1) * patchProfiles.length); + for (const record of peers) record.effects = Object.fromEntries(patchOptions.filter(option => record.profile.options[option]).map(option => { + const peer = peers.find(candidate => patchOptions.every(key => candidate.profile.options[key] === (key === option ? false : record.profile.options[key]))); + return [option, { reached: record.census.ok ? record.census.counts[option] > 0 : null, + removed: peer?.census.ok && peer.recoveryQualified ? peer.census.counts[option] === 0 : null, removalPeer: peer?.name }]; + })); + } + for (const record of records) record.qualified = record.recoveryQualified && Object.values(record.effects).every(effect => effect.reached === true && effect.removed === true); + const profileWitnesses = Object.fromEntries(patchProfiles.map(profile => [profile.name, records.filter(record => record.profile.name === profile.name && record.qualified).map(record => record.name)])); + if (!exactEqual(before, snapshot({ generation: true }))) throw new Error('Execution inputs changed during qualification'); + fs.writeFileSync(path.join(output, 'summary.json'), stringifyRecord({ protocol, records, profileWitnesses })); + if (records.some(record => !record.recoveryQualified) || Object.values(profileWitnesses).some(witnesses => witnesses.length === 0)) process.exitCode = 1; +} +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main().catch(error => { process.stderr.write(`${error.stack}\n`); process.exitCode = 1; }); diff --git a/scripts/qualify-jsconfuser-vm/qualify.mjs b/scripts/qualify-jsconfuser-vm/qualify.mjs new file mode 100644 index 00000000..72026fef --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/qualify.mjs @@ -0,0 +1,856 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; +import { + sources, + baselineOptions, + profiles, +} from "./config.mjs"; +import { emittedOptionEffect } from "./effects.mjs"; +import { + exactEqual, + parseRecord, + stringifyRecord, + CODEC_VERSION, + tagged, +} from "./codec.mjs"; +import { + assessCurrent, + PROJECTION_VERSION, +} from "./oracles.mjs"; +import { + snapshot, + auditEntries, + changedTools, + manifest, + sha256, + decoderRoot, + encoderRoot, + toolFiles, +} from "./audit.mjs"; +import { + cellSummary, + finalizeCells, + readCell, + cellFile, + assertRecoveryBoundary, + verifyRawPopulation, + recoveryRows, + assertStoredDisposition, +} from "./finalize.mjs"; + +export { emittedOptionEffect }; +export function eligibleEffectControl(record) { + return ( + record.encoded === true && + record.originEffectEligible !== false && + ![ + "source-oracle-failed", + "encoder-mismatch", + "generation-error", + "timeout", + ].includes(record.status) + ); +} +export function assertStableExecutionInputs(before, after) { + if (!exactEqual(before, after)) + throw new Error( + "Qualification inputs or repository state changed; rerun with stable inputs", + ); +} +export const exceptionExpected = [ + 1, + 2, + 3, + 4, + [ + "try:0", + "catch:SyntaxError", + "finally:0", + "try:1", + "catch:TypeError", + "finally:1", + "try:2", + "catch:Error", + "finally:2", + "try:3", + "finally:3", + ], +]; +const strip = (source) => source.replace(/^(?:\/\/[^\n]*\r?\n)+/, ""); +export function parseArgs(args) { + if (args.length === 1 && args[0] === "--audit-only") return { audit: true }; + const values = {}; + for (let i = 0; i < args.length; i += 2) { + if ( + ![ + "--output", + "--cases", + "--profiles", + "--seeds", + "--replay", + "--replay-mode", + "--recover", + "--raw-manifest", + "--recovery-boundary", + ].includes(args[i]) || + !args[i + 1] || + values[args[i]] + ) + throw new Error( + "Use --audit-only, or --output DIR with approved selection, or --replay DIR --output NEWDIR --replay-mode parser|native", + ); + values[args[i]] = args[i + 1]; + } + if (!values["--output"]) + throw new Error("An exclusive output directory is required"); + if (values["--recover"]) { + if ( + !values["--raw-manifest"] || + !values["--recovery-boundary"] || + ["--replay", "--replay-mode", "--cases", "--profiles", "--seeds"].some( + (key) => values[key], + ) + ) + throw new Error( + "Recovery requires frozen raw manifest/boundary and an exclusive output only", + ); + return { + output: path.resolve(values["--output"]), + recover: path.resolve(values["--recover"]), + rawManifest: path.resolve(values["--raw-manifest"]), + recoveryBoundary: path.resolve(values["--recovery-boundary"]), + }; + } + if (values["--raw-manifest"] || values["--recovery-boundary"]) + throw new Error("Recovery evidence requires --recover"); + if (values["--replay"]) { + if ( + !["parser", "native"].includes(values["--replay-mode"]) || + ["--cases", "--profiles", "--seeds"].some((key) => values[key]) + ) + throw new Error( + "Replay uses the full frozen selection and requires explicit parser/native mode", + ); + return { + output: path.resolve(values["--output"]), + replay: path.resolve(values["--replay"]), + mode: values["--replay-mode"], + }; + } + if (values["--replay-mode"]) throw new Error("Replay mode requires --replay"); + const names = (value, available) => { + const selected = + value === "all" ? Object.keys(available) : value.split(","); + if ( + !selected.length || + selected.some((key) => !Object.hasOwn(available, key)) + ) + throw new Error(`Unknown selection ${value}`); + return [...new Set(selected)]; + }; + const selectedProfiles = names(values["--profiles"] ?? "all", profiles); + const seeds = (values["--seeds"] ?? "1,2,3").split(","); + if ( + seeds.some( + (seed) => !/^(0|[1-9][0-9]*)$/.test(seed) || Number(seed) > 0xffffffff, + ) + ) + throw new Error("Seeds must be unsigned 32-bit integers"); + return { + output: path.resolve(values["--output"]), + cases: names(values["--cases"] ?? "all", sources), + profiles: selectedProfiles, + seeds: [...new Set(seeds.map(Number))], + }; +} +function write(file, value) { + fs.writeFileSync(file, value, { flag: "wx" }); +} +function writeRecord(file, record) { + write(file, stringifyRecord(record)); +} +function retainSupportingInputs(output, generation) { + const directory = path.join(output, "supporting-inputs"); + fs.mkdirSync(directory); + for (const [name, root] of [ + ["decoder", decoderRoot], + ...(generation ? [["encoder", encoderRoot]] : []), + ]) { + const destination = path.join(directory, name); + fs.mkdirSync(destination); + for (const member of [ + "src", + "node_modules", + "package.json", + "pnpm-lock.yaml", + "package-lock.json", + "tsconfig.json", + ]) { + const source = path.join(root, member); + if (fs.existsSync(source)) + fs.cpSync(source, path.join(destination, member), { + recursive: true, + verbatimSymlinks: true, + errorOnExist: true, + force: false, + }); + } + } + const toolsDirectory = path.join(directory, "tools"); + fs.mkdirSync(toolsDirectory); + for (const file of toolFiles()) { + fs.mkdirSync(path.dirname(path.join(toolsDirectory, file)), { + recursive: true, + }); + fs.copyFileSync( + path.join(decoderRoot, file), + path.join(toolsDirectory, file), + fs.constants.COPYFILE_EXCL, + ); + } + writeRecord(path.join(directory, "retention.json"), { + copied: + "Current decoder sources/dependencies/package metadata and qualifier tooling; generation also retains pinned encoder sources/dependencies/package metadata, including Closure jar.", + binariesNotArchived: [ + "Node executable", + "Java executable and JDK runtime libraries", + ], + externalDependencyLinks: + "Manifests identify any dependency symlink outside its copied tree; such targets require separate supply. Internal pnpm target bytes are copied.", + replay: + "Frozen source/encoded replay uses the current decoder. Reconstructing generation requires separately supplied identified Node/JDK binaries and any external dependency targets.", + }); +} +export async function runWorker( + request, + timeoutMs, + { + spawnWorker = spawn, + killGroup = (pid) => process.kill(-pid, "SIGKILL"), + setTimer = setTimeout, + clearTimer = clearTimeout, + } = {}, +) { + return new Promise((resolve) => { + const executable = process.execPath, + argv = [ + fileURLToPath( + new URL("./worker.mjs", import.meta.url), + ), + "--worker", + ]; + const startedAt = new Date().toISOString(); + const child = spawnWorker(executable, argv, { + cwd: decoderRoot, + detached: true, + stdio: ["pipe", "pipe", "pipe"], + }); + let stdout = "", + stderr = "", + timedOut = false, + spawnError = null; + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (value) => (stdout += value)); + child.stderr.on("data", (value) => (stderr += value)); + child.stdin.on("error", () => {}); + child.stdin.end(stringifyRecord(request)); + const timer = setTimer(() => { + timedOut = true; + if (request.journalPath) + fs.appendFileSync( + request.journalPath, + JSON.stringify({ + eventAt: new Date().toISOString(), + value: tagged({ + event: "parent-watchdog", + processGroup: child.pid, + requestedSignal: "SIGKILL", + closureExit: "unavailable if worker dies before a finish event", + }), + }) + "\n", + ); + try { + killGroup(child.pid); + } catch { + /* The worker may have already exited before the watchdog. */ + } + }, timeoutMs); + child.once("error", (error) => { + clearTimer(timer); + spawnError = { name: error.name, message: error.message }; + }); + child.once("close", (exitCode, signal) => { + clearTimer(timer); + if (request.journalPath) + fs.appendFileSync( + request.journalPath, + JSON.stringify({ + eventAt: new Date().toISOString(), + value: tagged({ + event: "worker-finish", + exitCode, + signal, + timedOut, + }), + }) + "\n", + ); + const process = { + executable: path.basename(executable), + argv: argv.map((value) => + path.isAbsolute(value) ? path.relative(decoderRoot, value) : value, + ), + cwd: ".", + startedAt, + completedAt: new Date().toISOString(), + timeoutMs, + exitCode, + signal, + timedOut, + error: spawnError, + journal: + request.journalPath && fs.existsSync(request.journalPath) + ? fs.readFileSync(request.journalPath, "utf8") + : null, + stdout, + stderr, + stdoutSha256: sha256(stdout), + }; + try { + resolve({ ...parseRecord(stdout), process }); + } catch { + resolve({ + ok: false, + error: { + message: timedOut + ? "Worker timeout" + : (spawnError?.message ?? "Invalid worker record"), + }, + process, + }); + } + }); + }); +} +function audit() { + const parserEntries = [ + "src/plugin/jsconfuser-vm.js", + "test/vm/jsconfuser-vm/minified-class-admission.test.js", + "test/vm/jsconfuser-vm/integration/plugin.test.js", + ].map((file) => path.join(decoderRoot, file)); + const harnessEntries = [ + "scripts/qualify-jsconfuser-vm/qualify.mjs", + "scripts/qualify-jsconfuser-vm/worker.mjs", + "test/scripts/qualify-jsconfuser-vm/qualify.test.js", + "test/scripts/qualify-jsconfuser-vm/replay.test.js", + ].map((file) => path.join(decoderRoot, file)); + return { + parser: auditEntries(parserEntries, { + external: ["fs", "node:os", "node:path", "node:url"], + }), + harness: auditEntries(harnessEntries, { + native: true, + external: [ + "node:path", + "node:url", + "node:child_process", + "node:module", + "node:vm", + "node:perf_hooks", + "node:assert/strict", + "node:os", + "node:events", + "node:stream", + ], + }), + encoder: auditEntries([path.join(encoderRoot, "src/index.ts")], { + native: true, + external: ["assert", "fs", "module", "path", "google-closure-compiler"], + }), + nativeFixture: auditEntries( + [ + path.join( + decoderRoot, + "test/vm/jsconfuser-vm/minified-class-admission.behavior.test.js", + ), + ], + { + native: true, + external: [ + "fs", + "node:vm", + "node:perf_hooks", + "node:os", + "node:path", + "node:url", + ], + }, + ), + boundary: { + generation: + "pinned source type stripping; runtime source is data; Closure Java process", + decoder: "source data, no target execution", + behavior: + "known source in fresh browser-like VM context, 10s per invocation; no Buffer/process/require", + limits: { + closure: 60000, + generation: 65000, + inspect: 20000, + behaviorWorker: 12000, + }, + }, + }; +} +export function verifyFrozen(root, expected) { + assertStableExecutionInputs( + expected, + manifest(root).filter( + (record) => record.path !== "population-manifest.json", + ), + ); +} +function physicalPath(file) { + let ancestor = path.resolve(file); + const suffix = []; + while (!fs.existsSync(ancestor)) { + suffix.unshift(path.basename(ancestor)); + ancestor = path.dirname(ancestor); + } + return path.join(fs.realpathSync(ancestor), ...suffix); +} +export function replayOutputAllowed(input, output) { + const relative = path.relative(fs.realpathSync(input), physicalPath(output)); + if ( + !relative || + (relative !== ".." && + !relative.startsWith(`..${path.sep}`) && + !path.isAbsolute(relative)) + ) + throw new Error("Replay output must be outside frozen input"); +} + +async function qualifyInspection( + inspected, + source, + encoded, + caseName, + native, + processes, + observations = {}, +) { + if (!native) return assessCurrent(inspected, source, { encoded }, null); + const observe = async (code) => { + const worker = await runWorker({ action: "behavior", source: code }, 12000); + processes.push(worker.process); + return worker.ok + ? worker.result + : { + kind: worker.process?.timedOut ? "timeout" : "error", + error: worker.error, + }; + }; + const sourceObservation = + observations.sourceObservation ?? (await observe(source)); + const encodedObservation = + observations.encodedObservation ?? (await observe(encoded)); + if ( + sourceObservation.kind !== "value" || + (caseName === "exceptionRoutes" && + !exactEqual(sourceObservation.value, exceptionExpected)) + ) + return { + qualified: false, + status: "source-oracle-failed", + sourceObservation, + encodedObservation, + }; + if (!exactEqual(sourceObservation, encodedObservation)) + return { + qualified: false, + status: "encoder-mismatch", + sourceObservation, + encodedObservation, + }; + return { + sourceObservation, + encodedObservation, + ...(await assessCurrent( + inspected, + source, + { + encoded, + sourceObservation, + encodedObservation, + ...(caseName === "exceptionRoutes" ? { value: exceptionExpected } : {}), + }, + observe, + )), + }; +} +async function main() { + const args = parseArgs(process.argv.slice(2)), + audits = audit(); + const before = snapshot({ generation: !args.replay }); + if (args.audit) { + process.stdout.write( + JSON.stringify({ audits, snapshot: before }, null, 2) + "\n", + ); + return; + } + if (args.recover) { + replayOutputAllowed(args.recover, args.output); + replayOutputAllowed(args.recover, args.rawManifest); + replayOutputAllowed(args.recover, args.recoveryBoundary); + const rawBytes = fs.readFileSync(args.rawManifest, "utf8"); + const raw = parseRecord(rawBytes); + const preparation = parseRecord( + fs.readFileSync(args.recoveryBoundary, "utf8"), + ); + const original = parseRecord( + fs.readFileSync(path.join(args.recover, "execution-inputs.json"), "utf8"), + ); + if ( + fs.realpathSync(args.recover) !== preparation.originalRoot || + fs.existsSync(path.join(args.recover, "summary.json")) || + fs.existsSync(path.join(args.recover, "population-manifest.json")) + ) + throw new Error( + "Recovery requires the identified unpublished original population", + ); + assertRecoveryBoundary( + original.before, + preparation, + before, + rawBytes, + changedTools(original.before.tools.commit), + ); + verifyRawPopulation(args.recover, raw); + const population = parseRecord( + fs.readFileSync(path.join(args.recover, "population.json"), "utf8"), + ); + const rows = recoveryRows(args.recover, population, raw); + if (fs.existsSync(args.output)) + throw new Error("Output directory must not exist"); + fs.mkdirSync(args.output, { recursive: true }); + writeRecord(path.join(args.output, "execution-inputs.json"), { + before, + audits, + args, + codec: CODEC_VERSION, + projection: PROJECTION_VERSION, + }); + write( + path.join(args.output, "original-population-manifest.json"), + rawBytes, + ); + writeRecord(path.join(args.output, "original-boundary.json"), preparation); + retainSupportingInputs(args.output, false); + const completed = finalizeCells(rows, { + readCell: (row) => readCell(args.recover, row), + readEncoded: (row) => + fs.readFileSync(path.join(args.recover, row.id, "encoded.js"), "utf8"), + eligible: eligibleEffectControl, + writeCell: (row, record) => { + const historical = readCell(args.recover, row, "qualification.json"); + assertStoredDisposition(record, historical); + const directory = path.join(args.output, row.id); + fs.mkdirSync(directory); + for (const name of ["source.js", "encoded.js"]) + if (fs.existsSync(path.join(args.recover, row.id, name))) + fs.copyFileSync( + path.join(args.recover, row.id, name), + path.join(directory, name), + fs.constants.COPYFILE_EXCL, + ); + const references = { + result: cellFile(args.recover, row.id, "result.json"), + qualification: cellFile(args.recover, row.id, "qualification.json"), + }; + writeRecord(path.join(directory, "stored-disposition.json"), { + ...cellSummary(record), + originalRoot: args.recover, + files: references, + }); + return references; + }, + }); + verifyRawPopulation(args.recover, raw); + assertStableExecutionInputs(before, snapshot({ generation: true })); + writeRecord(path.join(args.output, "summary.json"), { + schemaVersion: "jsconfuser-vm-qualification.v3", + populationStatus: "recovered-finalization-of-aborted-run", + originalRun: { + root: args.recover, + exitCode: 134, + outerProcessEvidence: + "Tool-observed exit/OOM; exact original outer stdout/stderr bytes were not separately retained", + publication: "absent", + manifestSha256: sha256(rawBytes), + }, + currentNativeReplayRequired: true, + startedInputs: original.before, + recoveryInputs: before, + completedAt: new Date().toISOString(), + args, + codec: CODEC_VERSION, + projection: PROJECTION_VERSION, + records: completed, + }); + writeRecord( + path.join(args.output, "population-manifest.json"), + manifest(args.output), + ); + process.stdout.write( + `Recovered ${completed.length} stored cell dispositions; original run remains aborted; current native replay required\n`, + ); + return; + } + if (args.replay) replayOutputAllowed(args.replay, args.output); + if (fs.existsSync(args.output)) + throw new Error("Output directory must not exist"); + fs.mkdirSync(args.output, { recursive: true }); + writeRecord(path.join(args.output, "execution-inputs.json"), { + before, + audits, + args, + codec: CODEC_VERSION, + projection: PROJECTION_VERSION, + }); + retainSupportingInputs(args.output, !args.replay); + const records = []; + let frozen; + if (args.replay) { + const summary = parseRecord( + fs.readFileSync(path.join(args.replay, "summary.json"), "utf8"), + ); + const expected = parseRecord( + fs.readFileSync( + path.join(args.replay, "population-manifest.json"), + "utf8", + ), + ); + frozen = expected; + verifyFrozen(args.replay, expected); + for (const cell of summary.records) { + const origin = path.join(args.replay, cell.id); + const source = fs.readFileSync(path.join(origin, "source.js"), "utf8"); + const directory = path.join(args.output, cell.id); + fs.mkdirSync(directory); + write(path.join(directory, "source.js"), source); + const record = { + id: cell.id, + case: cell.case, + profile: cell.profile, + seed: cell.seed, + options: cell.options, + revision: before.decoderCommit, + originEffectEligible: eligibleEffectControl(cell), + processes: [], + }; + const encodedPath = path.join(origin, "encoded.js"); + if (!fs.existsSync(encodedPath)) { + record.status = "generation-error"; + record.qualified = false; + records.push(cellSummary(record)); + writeRecord(path.join(directory, "result.json"), record); + continue; + } + const encoded = fs.readFileSync(encodedPath, "utf8"); + write(path.join(directory, "encoded.js"), encoded); + const inspection = await runWorker( + { action: "inspect", source: encoded }, + 20000, + ); + record.processes.push(inspection.process); + if (!inspection.ok) { + record.status = "inspection-error"; + record.qualified = false; + record.error = inspection.error; + } else { + record.inspection = inspection.result; + if (inspection.result.publicRecord.ok) + write( + path.join(directory, "decoded.js"), + inspection.result.publicRecord.output, + ); + Object.assign( + record, + await qualifyInspection( + inspection.result, + source, + encoded, + cell.case, + args.mode === "native", + record.processes, + ), + ); + } + record.encoded = true; + records.push(cellSummary(record)); + writeRecord(path.join(directory, "result.json"), record); + process.stdout.write(`${record.id}: ${record.status}\n`); + } + verifyFrozen(args.replay, frozen); + } else { + // Freeze all sources/configuration before the first encoder worker import. + writeRecord(path.join(args.output, "population.json"), { + sources: Object.fromEntries( + args.cases.map((name) => [name, `${sources[name]}\n`]), + ), + profiles: args.profiles.map((name) => ({ + name, + options: { ...baselineOptions, ...profiles[name] }, + })), + seeds: args.seeds, + }); + for (const caseName of args.cases) + for (const profile of args.profiles) + for (const seed of args.seeds) { + const source = `${sources[caseName]}\n`, + options = { ...baselineOptions, ...profiles[profile] }, + id = `${caseName}--${profile}--${seed}`; + const directory = path.join(args.output, id); + fs.mkdirSync(directory); + write(path.join(directory, "source.js"), source); + const record = { + id, + case: caseName, + profile, + seed, + options, + processes: [], + qualified: false, + }; + const sourceBehavior = await runWorker( + { action: "behavior", source }, + 12000, + ); + record.processes.push(sourceBehavior.process); + record.sourceObservation = sourceBehavior.ok + ? sourceBehavior.result + : { kind: "error", error: sourceBehavior.error }; + if ( + record.sourceObservation.kind !== "value" || + (caseName === "exceptionRoutes" && + !exactEqual(record.sourceObservation.value, exceptionExpected)) + ) { + record.status = "source-oracle-failed"; + records.push(cellSummary(record)); + writeRecord(path.join(directory, "result.json"), record); + continue; + } + const journalPath = path.join(directory, "generation-journal.ndjson"); + write(journalPath, ""); + const generation = await runWorker( + { + action: "generate", + journalPath, + identities: { java: before.java, jar: before.jar }, + source, + options, + seed, + }, + 65000, + ); + record.processes.push(generation.process); + if (!generation.ok) { + record.status = generation.process?.timedOut + ? "timeout" + : "generation-error"; + record.error = generation.error; + } else { + const encoded = strip(generation.result.raw); + write( + path.join(directory, "raw-encoded.js"), + generation.result.raw, + ); + write(path.join(directory, "encoded.js"), encoded); + record.encoded = true; + record.generation = generation.result; + const encodedBehavior = await runWorker( + { action: "behavior", source: encoded }, + 12000, + ); + record.processes.push(encodedBehavior.process); + record.encodedObservation = encodedBehavior.ok + ? encodedBehavior.result + : { kind: "error", error: encodedBehavior.error }; + if ( + !exactEqual(record.sourceObservation, record.encodedObservation) + ) + record.status = "encoder-mismatch"; + else { + const inspection = await runWorker( + { action: "inspect", source: encoded }, + 20000, + ); + record.processes.push(inspection.process); + if (!inspection.ok) { + record.status = "inspection-error"; + record.error = inspection.error; + } else { + record.inspection = inspection.result; + if (inspection.result.publicRecord.ok) + write( + path.join(directory, "decoded.js"), + inspection.result.publicRecord.output, + ); + Object.assign( + record, + await qualifyInspection( + inspection.result, + source, + encoded, + caseName, + true, + record.processes, + record, + ), + ); + } + } + } + records.push(cellSummary(record)); + writeRecord(path.join(directory, "result.json"), record); + process.stdout.write(`${id}: ${record.status}\n`); + } + } + const completed = finalizeCells(records, { + readCell: (row) => readCell(args.output, row), + readEncoded: (row) => + fs.readFileSync(path.join(args.output, row.id, "encoded.js"), "utf8"), + eligible: eligibleEffectControl, + writeCell: (row, record) => { + writeRecord(path.join(args.output, row.id, "qualification.json"), record); + return { + result: cellFile(args.output, row.id, "result.json"), + qualification: cellFile(args.output, row.id, "qualification.json"), + }; + }, + }); + assertStableExecutionInputs(before, snapshot({ generation: !args.replay })); + writeRecord(path.join(args.output, "summary.json"), { + schemaVersion: "jsconfuser-vm-qualification.v3", + startedInputs: before, + completedAt: new Date().toISOString(), + args, + codec: CODEC_VERSION, + projection: PROJECTION_VERSION, + records: completed, + }); + // Exclude the manifest itself: its verified member set is otherwise complete. + writeRecord( + path.join(args.output, "population-manifest.json"), + manifest(args.output), + ); +} +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) + main().catch((error) => { + process.stderr.write(`${error.message}\n`); + process.exitCode = 1; + }); diff --git a/scripts/qualify-jsconfuser-vm/retain-minified-class-inputs.mjs b/scripts/qualify-jsconfuser-vm/retain-minified-class-inputs.mjs new file mode 100644 index 00000000..5c1596da --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/retain-minified-class-inputs.mjs @@ -0,0 +1,206 @@ +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { execFileSync, spawn } from 'node:child_process' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { javaRoot, decoderRoot, encoderRoot, PIN as encoderPin } from './audit.mjs' + +// Audit mode reads source/tooling bytes only. The sole encoder import is behind +// the explicit --generate/--worker switches; neither source nor emitted VM is evaluated. +// Tracked inputs are identified by their Git commits; only untracked bytes +// (installed packages, the JDK, the Closure jar) carry digests. +const outputRoot = path.join(decoderRoot, 'test/vm/jsconfuser-vm/fixtures/minified-class-runtime') +const sources = { + exceptions: 'function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished];\n', + 'nested-strings': 'function outer(prefix) { return function middle(n) { return function inner() { return prefix + ":" + n; }; }; } window.TEST_OUTPUT = outer("id")(7)();\n', +} +const options = Object.freeze({ + randomizeOpcodes: false, shuffleOpcodes: false, encodeBytecode: false, + concealConstants: false, controlFlowFlattening: false, dispatcher: false, + stringConcealing: false, macroOpcodes: false, specializedOpcodes: false, + aliasedOpcodes: false, antiInstrumentation: false, selfModifying: false, + timingChecks: false, classObfuscation: true, handlerTable: false, + minify: true, target: 'browser', +}) +const digest = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex') +function fileRecord(filename, base = decoderRoot) { + const bytes = fs.readFileSync(filename) + return { path: path.relative(base, filename), bytes: bytes.length, sha256: digest(bytes) } +} +function files(root) { + return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const item = path.join(root, entry.name) + if (entry.isDirectory()) return files(item) + return entry.isFile() ? [item] : [] + }).sort() +} +function localImports(entry) { + const seen = new Set(), todo = [entry], external = new Set() + while (todo.length) { + const filename = todo.pop() + if (seen.has(filename)) continue + seen.add(filename) + const text = fs.readFileSync(filename, 'utf8') + // The pinned source uses static ESM imports/exports. Audit includes type-only + // dependencies conservatively; no string or module in this census executes. + const imports = [...text.matchAll(/^\s*(?:import|export)\s+(?:[^;]*?\s+from\s+)?["']([^"']+)["']/gm)] + for (const [, specifier] of imports) { + if (specifier.startsWith('.')) todo.push(path.resolve(path.dirname(filename), specifier)) + else external.add(specifier) + } + if (/\bimport\s*\(|\brequire\s*\(|\beval\s*\(|\bnew\s+Function\s*\(/.test(text)) { + throw new Error(`Unreviewed dynamic import/execution boundary in ${filename}`) + } + } + return { files: [...seen].sort().map((f) => path.relative(encoderRoot, f)), external: [...external].sort() } +} +function packageClosure(names) { + const todo = names, seen = new Set(), result = [] + while (todo.length) { + const name = todo.pop() + const root = path.join(encoderRoot, 'node_modules', name) + if (seen.has(name)) continue + if (!fs.existsSync(path.join(root, 'package.json'))) throw new Error(`Missing installed dependency ${name}`) + seen.add(name) + const metadata = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')) + const manifest = files(root).map((f) => fileRecord(f, root)) + result.push({ name, version: metadata.version, manifest, manifestSha256: digest(JSON.stringify(manifest)) }) + todo.push(...Object.keys(metadata.dependencies || {})) + // Only present optional binaries matter to this installed closure. The + // actual encoder call uses Java, recorded separately, without native fallback. + for (const dependency of Object.keys(metadata.optionalDependencies || {})) { + if (fs.existsSync(path.join(encoderRoot, 'node_modules', dependency, 'package.json'))) todo.push(dependency) + } + } + return result.sort((a, b) => a.name.localeCompare(b.name)) +} +function gitRead(args) { return execFileSync('git', ['-C', encoderRoot, ...args], {encoding:'utf8',timeout:10000}) } +function checkoutIdentity() { + const head=gitRead(['rev-parse','HEAD']).trim() + if(head!==encoderPin) throw new Error('Encoder HEAD does not equal declared pin') + const status=gitRead(['status','--porcelain=v1','--untracked-files=all']) + if(status)throw new Error('Encoder checkout is not clean') + return {head,status} +} +function harnessIdentity() { + const read=(args)=>execFileSync('git',['-C',decoderRoot,...args],{encoding:'utf8',timeout:10000}).trim() + const harness=path.relative(decoderRoot,fileURLToPath(import.meta.url)) + return {path:harness,commit:read(['rev-parse','HEAD']),clean:read(['status','--porcelain','--',harness])===''} +} +function snapshot() { + if (!javaRoot) throw new Error('Needs JAVA_HOME or java on PATH') + const sourceImports = localImports(path.join(encoderRoot, 'src/index.ts')) + const dependencies = packageClosure(sourceImports.external.filter((name) => !['assert', 'fs', 'module', 'path'].includes(name))) + const java = fs.realpathSync(path.join(javaRoot, 'bin/java')) + return { + schemaVersion: 'vm-minified-class-source-generation.v1', + encoderPin, checkout:checkoutIdentity(), encoderImports: sourceImports, + runtimeText: 'src/runtime.ts', + harness: harnessIdentity(), + dependencies, + node: { version: process.version, executable: 'node', sha256: digest(fs.readFileSync(process.execPath)) }, + java: { executable: 'java', sha256: digest(fs.readFileSync(java)), release: fs.readFileSync(path.join(javaRoot, 'release'), 'utf8') }, + jar: fileRecord(path.join(encoderRoot, 'node_modules/google-closure-compiler-java/compiler.jar'), encoderRoot), + closureInvocation: { executable: 'java', argv: ['-XX:+IgnoreUnrecognizedVMOptions', '--sun-misc-unsafe-memory-access=allow', '-jar', 'node_modules/google-closure-compiler-java/compiler.jar', '--compilation_level=ADVANCED', '--warning_level=QUIET'], stdin: 'serialized runtime source', stdout: 'emitted source', stderr: 'compiler diagnostics', compilerExit: 'wrapper rejects nonzero exit' }, + options:{...options}, childTimeoutMs:60000, cellTimeoutMs:65000, seeds: [169, 170, 171], + sources: Object.fromEntries(Object.entries(sources).map(([name, source]) => [name, { source, bytes: Buffer.byteLength(source) }])), + outputRoot: path.relative(decoderRoot, outputRoot), + limitations: ['Present package/tool identities do not establish historical generation provenance.', 'Tool/dependency/JDK bytes are identified, not archived; future exact regeneration requires these bytes to be supplied.', 'Source-only generation: no input/emitted VM execution, public admission or native equivalence.', 'Installed packages and Closure execute as normal tooling, without a sandbox claim.'], + } +} +function rng(seed) { + let state = seed >>> 0, draws = 0 + const random = () => { + draws++ + state = (state + 0x6d2b79f5) >>> 0 + let value = Math.imul(state ^ (state >>> 15), state | 1) + value ^= value + Math.imul(value ^ (value >>> 7), value | 61) + return ((value ^ (value >>> 14)) >>> 0) / 0x100000000 + } + return { random, drawCount: () => draws } +} +const mode = process.argv[2] +if (mode === '--worker') { + const name=process.argv[3],seed=Number(process.argv[4]) + if(process.argv.length!==5||!Object.hasOwn(sources,name)||![169,170,171].includes(seed))throw new Error('Invalid generation worker cell') + const savedPath=process.env.PATH,savedRandom=Math.random,random=rng(seed) + const beforeCheckout=checkoutIdentity() + let Compiler,originalRun + const processRecords=[] + try { + process.env.PATH=`${path.join(javaRoot,'bin')}${path.delimiter}${savedPath||''}` + ;({default:Compiler}=await import(pathToFileURL(path.join(encoderRoot,'node_modules/google-closure-compiler/lib/node/index.js')).href)) + originalRun=Compiler.prototype.run + Compiler.prototype.run=function(callback){ + let child,timer + const startedAt=new Date().toISOString() + child=originalRun.call(this,(exitCode,stdout,stderr)=>{ + clearTimeout(timer) + processRecords.push({startedAt,completedAt:new Date().toISOString(),timeoutMs:60000,executable:path.basename(this.javaPath),argv:this.commandArguments.map((value)=>path.isAbsolute(value)?path.relative(encoderRoot,value):value),exitCode,stdoutBytes:Buffer.byteLength(stdout),stderr}) + callback(exitCode,stdout,stderr) + }) + timer=setTimeout(()=>child.kill('SIGKILL'),60000) + child.once('close',()=>clearTimeout(timer)) + return child + } + const {JsConfuserVM}=await import(pathToFileURL(path.join(encoderRoot,'src/index.ts')).href) + Math.random=random.random + const result=await JsConfuserVM.obfuscate(sources[name],{...options}) + if(processRecords.length!==1||processRecords[0].exitCode!==0)throw new Error('Cell has no unique successful Closure process') + if(typeof result.code!=='string'||!result.code)throw new Error('Encoder returned no source') + console.log(JSON.stringify({code:result.code,draws:random.drawCount(),processRecords})) + } finally { + Math.random=savedRandom + if(Compiler&&originalRun)Compiler.prototype.run=originalRun + if(savedPath===undefined)delete process.env.PATH;else process.env.PATH=savedPath + if(digest(JSON.stringify(beforeCheckout))!==digest(JSON.stringify(checkoutIdentity())))throw new Error('Encoder checkout changed in worker, including on failure') + } +} else { + const usage='Use --audit-only, or separately authorized --generate --record ' + if(!(mode==='--audit-only'&&process.argv.length===3)&&!(mode==='--generate'&&process.argv[3]==='--record'&&process.argv.length===5))throw new Error(usage) + const before=snapshot(),beforeDigest=digest(JSON.stringify(before)) + if(mode==='--audit-only')console.log(JSON.stringify(before,null,2)) + else { + // The fixture tree receives only source and encoded bytes; the run record + // goes to the caller's path so no record lands beside tracked fixtures. + const recordPath=path.resolve(process.argv[4]) + if(!path.relative(outputRoot,recordPath).startsWith('..'))throw new Error('The run record must lie outside the fixture tree') + if(fs.existsSync(recordPath))throw new Error('Refusing to overwrite the run record') + for(const name of Object.keys(sources))for(const seed of before.seeds)for(const filename of ['source.js',`encoded-${seed}.js`])if(fs.existsSync(path.join(outputRoot,name,filename)))throw new Error(`Refusing to overwrite ${name}/${filename}`) + const startedAt=new Date().toISOString(),cells=[] + try { + for(const [name,source] of Object.entries(sources)) { + const directory=path.join(outputRoot,name) + fs.mkdirSync(directory,{recursive:true}) + fs.writeFileSync(path.join(directory,'source.js'),source,{flag:'wx'}) + for(const seed of before.seeds) { + const cellStart=new Date().toISOString() + const worker=await new Promise((resolve,reject)=>{ + const argv=[fileURLToPath(import.meta.url),'--worker',name,String(seed)] + const child=spawn(process.execPath,argv,{cwd:decoderRoot,detached:true,stdio:['ignore','pipe','pipe']}) + let stdout='',stderr='',timedOut=false + child.stdout.setEncoding('utf8');child.stderr.setEncoding('utf8') + child.stdout.on('data',(data)=>stdout+=data);child.stderr.on('data',(data)=>stderr+=data) + const timer=setTimeout(()=>{timedOut=true;try{process.kill(-child.pid,'SIGKILL')}catch{}},65000) + child.once('error',(error)=>{clearTimeout(timer);reject(error)}) + child.once('close',(exitCode,signal)=>{clearTimeout(timer);const processRecord={executable:'node',argv:argv.map((value)=>path.isAbsolute(value)?path.relative(decoderRoot,value):value),cwd:'.',timeoutMs:65000,exitCode,signal,timedOut,stdoutSha256:digest(stdout),stderr};if(exitCode!==0||timedOut)reject(Object.assign(new Error('Generation cell process failed'),{processRecord}));else {try{resolve({result:JSON.parse(stdout),processRecord})}catch(error){reject(Object.assign(new Error(`Invalid worker stdout: ${error.message}`),{processRecord}))}}}) + }) + const {code,draws,processRecords}=worker.result + if(processRecords.length!==1||processRecords[0].exitCode!==0)throw new Error('Current worker has no unique successful Closure process') + const record={case:name,seed,options:{...options},startedAt:cellStart,completedAt:new Date().toISOString(),rng:{algorithm:'Mulberry32',seed,draws},encodedBytes:Buffer.byteLength(code),sourceObservation:null,encodedObservation:null,decodedObservation:null,publicAdmission:null,workerProcess:worker.processRecord,closureProcess:processRecords[0],generationManifest:'../generation-manifest.json'} + fs.writeFileSync(path.join(directory,`encoded-${seed}.js`),code,{flag:'wx'}) + cells.push(record) + } + } + const after=snapshot() + if(beforeDigest!==digest(JSON.stringify(after)))throw new Error('Source/tool/dependency bytes changed during generation') + fs.writeFileSync(recordPath,`${JSON.stringify({...before,startedAt,completedAt:new Date().toISOString(),cells,inputsUnchanged:true},null,2)}\n`,{flag:'wx'}) + console.log(JSON.stringify({outputRoot:before.outputRoot,record:recordPath,cells:cells.map(({case:name,seed,encodedBytes})=>({case:name,seed,encodedBytes}))})) + } catch(error) { + fs.writeFileSync(recordPath,`${JSON.stringify({...before,startedAt,failedAt:new Date().toISOString(),cells,error:error.message,processRecord:error.processRecord||null},null,2)}\n`,{flag:'wx'}) + throw error + } finally { + if(digest(JSON.stringify(before.checkout))!==digest(JSON.stringify(checkoutIdentity())))throw new Error('Encoder checkout changed, including on failure') + } + } +} diff --git a/scripts/qualify-jsconfuser-vm/retain_evidence.py b/scripts/qualify-jsconfuser-vm/retain_evidence.py new file mode 100644 index 00000000..73f6da77 --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/retain_evidence.py @@ -0,0 +1,179 @@ +"""Retain exact empirical inputs/results in bounded tar parts and verify every member.""" +import argparse +from datetime import datetime, timezone +import gzip +import hashlib +import io +import json +from pathlib import Path +import tarfile + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def file_identity(path): + h = hashlib.sha256() + with path.open('rb') as stream: + for data in iter(lambda: stream.read(1024 * 1024), b''): + h.update(data) + return {'bytes': path.stat().st_size, 'sha256': h.hexdigest()} + + +class PartsWriter: + def __init__(self, directory, maximum): + self.directory, self.maximum = directory, maximum + self.stream, self.size, self.parts = None, 0, [] + + def write(self, data): + view = memoryview(data) + while view: + if self.stream is None: + path = self.directory / f'evidence.tar.gz.part{len(self.parts)+1:03d}' + self.stream = path.open('xb') + self.parts.append({'path': path.name}) + self.size = 0 + amount = min(len(view), self.maximum-self.size) + self.stream.write(view[:amount]) + self.size += amount + view = view[amount:] + if self.size == self.maximum: + self.stream.close() + self.stream = None + return len(data) + + def flush(self): + if self.stream: + self.stream.flush() + + def close(self): + if self.stream: + self.stream.close() + self.stream = None + for part in self.parts: + part.update(file_identity(self.directory / part['path'])) + + +class PartsReader(io.RawIOBase): + def __init__(self, paths): + self.paths = iter(paths) + self.stream = None + + def readable(self): + return True + + def readinto(self, buffer): + while True: + if self.stream is None: + path = next(self.paths, None) + if path is None: + return 0 + self.stream = path.open('rb') + data = self.stream.read(len(buffer)) + if data: + buffer[:len(data)] = data + return len(data) + self.stream.close() + self.stream = None + + def close(self): + if self.stream: + self.stream.close() + super().close() + + +def verify(manifest_path): + manifest = json.loads(manifest_path.read_text()) + paths = [] + for part in manifest['parts']: + path = manifest_path.parent / part['path'] + if file_identity(path) != {k: part[k] for k in ['bytes', 'sha256']}: + raise ValueError(f'Archive part identity changed: {part["path"]}') + paths.append(path) + expected = {member['path']: member for member in manifest['members']} + if len(expected) != len(manifest['members']): + raise ValueError('Duplicate manifest member') + found = set() + with io.BufferedReader(PartsReader(paths)) as stream, tarfile.open(fileobj=stream, mode='r|gz') as archive: + for member in archive: + if member.name in found or member.name not in expected: + raise ValueError(f'Duplicate or unexpected archive member: {member.name}') + found.add(member.name) + record = expected[member.name] + if member.issym(): + actual = {'path': member.name, 'kind': 'symlink', 'target': member.linkname, 'sha256': digest(member.linkname.encode())} + elif member.isfile(): + h = hashlib.sha256() + count = 0 + with archive.extractfile(member) as contents: + for data in iter(lambda: contents.read(1024*1024), b''): + h.update(data) + count += len(data) + actual = {'path': member.name, 'bytes': count, 'sha256': h.hexdigest()} + else: + raise ValueError(f'Unexpected member type: {member.name}') + if actual != record: + raise ValueError(f'Archive member identity changed: {member.name}') + if found != set(expected): + raise ValueError('Missing archive members') + return {'verifiedAt': datetime.now(timezone.utc).isoformat(), 'members': len(found), + 'manifestSha256': digest(manifest_path.read_bytes()), 'completeReadback': True} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--verify', type=Path) + parser.add_argument('--root', type=Path) + parser.add_argument('--output', type=Path) + parser.add_argument('--input', action='append', type=Path, default=[]) + parser.add_argument('--part-mib', type=int, default=40) + parser.add_argument('--boundary') + args = parser.parse_args() + if args.verify: + print(json.dumps(verify(args.verify.resolve(strict=True)))) + return + if not args.root or not args.output or not args.input or not args.boundary or not 1 <= args.part_mib <= 48: + parser.error('Creation requires root, exclusive output, inputs, boundary and part size in 1..48 MiB') + root = args.root.resolve(strict=True) + output = args.output.resolve() + files = {} + for requested in args.input: + path = root / requested + if not path.exists() and not path.is_symlink(): + raise ValueError(f'Missing requested input: {requested}') + selected = list(path.rglob('*')) if path.is_dir() else [path] + for item in selected: + if item.is_symlink() or item.is_file(): + relative = item.relative_to(root).as_posix() + if output == item or output in item.parents: + raise ValueError('Archive output overlaps an input') + files[relative] = item + output.mkdir(parents=True, exist_ok=False) + records = [] + writer = PartsWriter(output, args.part_mib*1024*1024) + try: + with gzip.GzipFile(fileobj=writer, mode='wb', mtime=0) as compressed, tarfile.open(fileobj=compressed, mode='w|', format=tarfile.PAX_FORMAT) as archive: + for name, path in sorted(files.items()): + if path.is_symlink(): + target = path.readlink().as_posix() + record = {'path': name, 'kind': 'symlink', 'target': target, 'sha256': digest(target.encode())} + else: + record = {'path': name, **file_identity(path)} + archive.add(path, arcname=name, recursive=False) + records.append(record) + finally: + writer.close() + manifest = {'boundary': args.boundary, 'createdAt': datetime.now(timezone.utc).isoformat(), + 'format': 'ordered gzip PAX tar parts', 'parts': writer.parts, 'members': records, + 'externalPrerequisites': ['Identified Node/JDK binaries and external dependency symlink targets require separate supply.'], + 'looseOriginalsPreserved': True} + manifest_path = output / 'archive-manifest.json' + manifest_path.write_text(json.dumps(manifest, indent=2)+'\n') + result = verify(manifest_path) + (output / 'readback.json').write_text(json.dumps(result, indent=2)+'\n') + print(json.dumps(result)) + + +if __name__ == '__main__': + main() diff --git a/scripts/qualify-jsconfuser-vm/test_capture_native_replay.py b/scripts/qualify-jsconfuser-vm/test_capture_native_replay.py new file mode 100644 index 00000000..945bd990 --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/test_capture_native_replay.py @@ -0,0 +1,114 @@ +"""Pure fake-process controls for the outer replay evidence lifecycle.""" + +import importlib.util +import json +import signal +import subprocess +import tempfile +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + +spec = importlib.util.spec_from_file_location( + "capture", Path(__file__).with_name("capture_native_replay.py") +) +capture = importlib.util.module_from_spec(spec) +spec.loader.exec_module(capture) + + +class CaptureLifecycle(unittest.TestCase): + def execute(self, configure): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "input").mkdir() + (root / "node").write_text("fake runtime, never executed") + handlers = {} + + def register(number, handler): + handlers[number] = handler + return signal.SIG_DFL + + child = Mock(pid=123, poll=Mock(return_value=None)) + launch = Mock(return_value=child) + terminate = Mock() + configure(child, handlers, launch, terminate) + argv = ["capture", "--replay", str(root / "input"), "--output", str(root / "output"), + "--capture", str(root / "capture"), "--identity", str(root / "node")] + with patch.object(capture.sys, "argv", argv), \ + patch.object(capture.Path, "cwd", return_value=root), \ + patch.object(capture.shutil, "which", return_value=str(root / "node")), \ + patch.object(capture.subprocess, "check_output", return_value="fake-version"), \ + patch.object(capture.subprocess, "Popen", launch), \ + patch.object(capture.os, "killpg", terminate), \ + patch.object(capture.signal, "signal", side_effect=register), \ + patch("builtins.print"): + status = capture.main() + result = json.loads((root / "capture/execution-result.json").read_text()) + self.assertTrue((root / "capture/stdout.log").is_file()) + self.assertTrue((root / "capture/stderr.log").is_file()) + self.assertTrue(result["identitiesUnchanged"]) + return status, result, terminate + + def test_normal_completion(self): + def configure(child, *_): + child.wait.return_value = 0 + status, result, terminate = self.execute(configure) + self.assertEqual((status, result["observedExitCode"]), (0, 0)) + terminate.assert_not_called() + + def test_child_exit_races_with_cancellation(self): + def configure(child, handlers, _launch, terminate): + child.wait.side_effect = [capture.ReplayCancelled(signal.SIGTERM), -15] + terminate.side_effect = ProcessLookupError("already exited") + status, result, _ = self.execute(configure) + self.assertEqual((status, result["observedSignal"]), (1, 15)) + self.assertEqual(result["cleanupErrors"], []) + + def test_repeated_parent_signals_do_not_skip_finalization(self): + def configure(child, handlers, _launch, terminate): + def first_wait(*_args, **_kwargs): + handlers[signal.SIGTERM](signal.SIGTERM, None) + def wait(*args, **kwargs): + if not kwargs: + first_wait() + return -15 + child.wait.side_effect = wait + terminate.side_effect = lambda *_: handlers[signal.SIGINT](signal.SIGINT, None) + status, result, _ = self.execute(configure) + self.assertEqual(status, 1) + self.assertEqual([item["signal"] for item in result["wrapperSignals"]], + [signal.SIGTERM, signal.SIGINT]) + + def test_termination_escalates_with_bounded_wait(self): + def configure(child, *_): + child.wait.side_effect = [capture.ReplayCancelled(signal.SIGTERM), + subprocess.TimeoutExpired("fake", 5), -9] + status, result, terminate = self.execute(configure) + self.assertEqual((status, result["observedSignal"]), (1, 9)) + self.assertEqual([call.args[1] for call in terminate.call_args_list], + [signal.SIGTERM, signal.SIGKILL]) + self.assertEqual(len(result["cleanupErrors"]), 1) + + def test_launch_failure_still_publishes_outer_record(self): + def configure(_child, _handlers, launch, _terminate): + launch.side_effect = OSError("fake launch failure") + status, result, terminate = self.execute(configure) + self.assertEqual(status, 1) + self.assertIsNone(result["observedReturnCode"]) + self.assertIn("fake launch failure", result["launchOrWaitFailure"]) + terminate.assert_not_called() + + def test_cleanup_failure_retains_unavailable_return_code(self): + def configure(child, _handlers, _launch, terminate): + child.wait.side_effect = [capture.ReplayCancelled(signal.SIGTERM), + OSError("fake wait failure"), OSError("fake wait failure")] + terminate.side_effect = PermissionError("fake termination failure") + status, result, _ = self.execute(configure) + self.assertEqual(status, 1) + self.assertIsNone(result["observedReturnCode"]) + self.assertFalse(result["childTerminationConfirmed"]) + self.assertEqual(len(result["cleanupErrors"]), 4) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/qualify-jsconfuser-vm/test_retain_evidence.py b/scripts/qualify-jsconfuser-vm/test_retain_evidence.py new file mode 100644 index 00000000..a0092d7b --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/test_retain_evidence.py @@ -0,0 +1,52 @@ +"""Exercise exact multipart archive readback and corruption refusal.""" +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +TOOL = Path(__file__).with_name('retain_evidence.py') +spec = importlib.util.spec_from_file_location('retainer', TOOL) +retainer = importlib.util.module_from_spec(spec) +spec.loader.exec_module(retainer) + + +class RetentionTest(unittest.TestCase): + def test_multipart_exact_bytes_and_symlink(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = root / 'input' + source.mkdir() + (source / 'bytes.bin').write_bytes(os.urandom(2 * 1024 * 1024)) + (source / 'text.txt').write_text('Exact retained evidence\n') + (source / 'external-link').symlink_to('/separately/supplied/dependency') + output = root / 'archive' + process = subprocess.run([ + sys.executable, str(TOOL), '--root', str(root), + '--input', 'input', '--output', str(output), '--part-mib', '1', + '--boundary', 'Synthetic retention test', + ], capture_output=True, text=True, timeout=30) + self.assertEqual(process.returncode, 0, process.stderr) + manifest_path = output / 'archive-manifest.json' + manifest = json.loads(manifest_path.read_text()) + self.assertGreater(len(manifest['parts']), 1) + self.assertEqual(retainer.verify(manifest_path)['members'], 3) + member = next(m for m in manifest['members'] if m['path'].endswith('bytes.bin')) + self.assertEqual(member['sha256'], retainer.file_identity(source / 'bytes.bin')['sha256']) + part = output / manifest['parts'][0]['path'] + original = part.read_bytes() + part.write_bytes(bytes([original[0] ^ 1]) + original[1:]) + with self.assertRaisesRegex(ValueError, 'Archive part identity changed'): + retainer.verify(manifest_path) + part.write_bytes(original) + manifest['members'].append(manifest['members'][0]) + manifest_path.write_text(json.dumps(manifest)) + with self.assertRaisesRegex(ValueError, 'Duplicate manifest member'): + retainer.verify(manifest_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/qualify-jsconfuser-vm/timing-observation.mjs b/scripts/qualify-jsconfuser-vm/timing-observation.mjs new file mode 100644 index 00000000..59298c06 --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/timing-observation.mjs @@ -0,0 +1,277 @@ +import { runInNewContext } from 'node:vm' +import { performance } from 'node:perf_hooks' +import { spawn } from 'node:child_process' +import fs from 'node:fs' +import { fileURLToPath } from 'node:url' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import generate from '@babel/generator' +import * as t from '@babel/types' +import { diagnoseContainer } from '../../src/vm/jsconfuser-vm/extract-container.js' +import { locateHandlers } from '../../src/vm/jsconfuser-vm/locate-handlers.js' +import { locateBootSkeleton } from '../../src/vm/jsconfuser-vm/locate-boot-payload.js' +import { parseRecord, stringifyRecord } from './codec.mjs' + +const scalarTimingValue = (node) => + node?.type === 'BooleanLiteral' || node?.type === 'NumericLiteral' + ? node.value + : node?.type === 'UnaryExpression' && + node.operator === '-' && + node.argument?.type === 'NumericLiteral' + ? -node.argument.value + : undefined + +// The literal behind the boot's words argument, through top-level bindings and the transport +// call's single argument: a string is the encoded transport, an array the plain word list. +function wordsLiteral(path) { + for (let seen = 0; path && seen < 8; seen++) { + if (path.isStringLiteral() || path.isArrayExpression()) return path.node + if (path.isIdentifier()) { + const binding = path.scope.getBinding(path.node.name) + path = binding?.path.isVariableDeclarator() ? binding.path.get('init') : null + } else if (path.isCallExpression() && path.node.arguments.length === 1) + path = path.get('arguments.0') + else return null + } + return null +} + +// Only the numeric, nonminified, known-source population uses this observer. +// Locate the scalar from both uses, then prove on a private disabled clone that +// extractContainerFromSource and the handler locator both find the runtime. The clone is an +// observation aid, never decoded-output credit. +export function timingSourceRoles(source) { + const ast = parse(source) + const candidates = [] + traverse(ast, { + VariableDeclarator(path) { + if ( + !path.get('id').isIdentifier() || + !path.parentPath.parentPath.isProgram() + ) + return + const binding = path.scope.getBinding(path.node.id.name) + if (binding?.referencePaths.length !== 2) return + const uses = binding.referencePaths + if ( + !uses.some( + (use) => use.parentPath.isIfStatement() && use.key === 'test', + ) || + !uses.some( + (use) => + use.parentPath.isBinaryExpression({ operator: '>' }) && + use.key === 'right', + ) + ) + return + candidates.push({ path, value: scalarTimingValue(path.node.init) }) + }, + }) + if (candidates.length !== 1) + throw new Error('Timing scalar is missing or ambiguous') + const candidate = candidates[0] + const name = candidate.path.node.id.name + candidate.path.get('init').replaceWith(t.booleanLiteral(false)) + const disabled = generate(ast).code + const container = diagnoseContainer(disabled) + if (!container.ok) + throw new Error(`Timing shell is not proved: ${container.diagnostic.code}`) + let view + try { + view = locateHandlers(parse(disabled)) + } catch (error) { + if (!error.code) throw error + throw new Error(`Timing shell is not proved: ${error.code}`) + } + const { call, holder, wordsIndex } = locateBootSkeleton(ast) + let holderPath = null + traverse(ast, { + NewExpression(path) { + if (path.node === holder) { + holderPath = path + path.stop() + } + }, + }) + const words = wordsLiteral(holderPath?.get(`arguments.${wordsIndex}`)) + if (!words) throw new Error('Timing boot words are not a literal') + return { + value: candidate.value, + name, + vmName: + call.callee.type === 'MemberExpression' && + call.callee.object.type === 'Identifier' + ? call.callee.object.name + : null, + disabled, + container: container.result, + emitted: { + numericTransport: words.type === 'StringLiteral', + handlerTable: view.handlers.some((handler) => handler.table), + }, + } +} + +export function observeTimingKnown( + source, + { + mode = 'fast', + vmName = null, + clock = null, + stop = null, + vmTimeoutMs = 10000, + } = {}, +) { + let reads = 0, + blockedMs = 0 + const context = { + TEST_OUTPUT: undefined, + performance: { + now: () => { + const value = clock ? clock(reads) : performance.now() + reads++ + return value + }, + }, + atob, + btoa, + pauseGate: () => { + const start = performance.now() + if (mode === 'slow') + while (performance.now() - start < 250) { + /* synchronous host work */ + } + if (mode === 'paused') { + if (!stop) + throw new Error('Paused observation needs a supervised process stop') + stop() + } + blockedMs += performance.now() - start + }, + } + context.window = context + context.self = context + let behavior + try { + runInNewContext(source, context, { timeout: vmTimeoutMs }) + behavior = { kind: 'value', value: context.TEST_OUTPUT } + } catch (error) { + behavior = { + kind: error.code === 'ERR_SCRIPT_EXECUTION_TIMEOUT' ? 'timeout' : 'error', + name: error.name, + message: error.message, + } + } + const machine = vmName && context[vmName] + const captureValue = (value) => + value !== null && + ['object', 'function', 'symbol', 'bigint'].includes(typeof value) + ? { runtimeType: typeof value } + : value + const state = machine + ? { + wordsZero: machine.bytecode.every((value) => value === 0), + frame: machine._f, + top: machine._regsTop, + currentFramePayloadCleared: machine._regs + .slice(machine._regsTop + 1) + .every((value) => value === undefined), + captures: Object.values(machine._openUpvalues ?? {}) + .filter(Boolean) + .map((value) => ({ + closed: value._closed, + value: captureValue(value._value), + backing: captureValue(value._regs[value._absSlot]), + })), + cleaned: context.CLEANED, + } + : null + return { behavior, state, clockReads: reads, blockedMs } +} + +export function runTimingNative(request) { + return new Promise((resolve, reject) => { + const startedAt = new Date().toISOString() + const child = spawn( + process.execPath, + [ + fileURLToPath(new URL('./timing-observation.mjs', import.meta.url)), + '--worker', + ], + { detached: true, stdio: ['pipe', 'pipe', 'pipe', 'ipc'] }, + ) + let stdout = '', + stderr = '', + timedOut = false, + pause = null, + resumeTimer + child.stdout.on('data', (bytes) => (stdout += bytes)) + child.stderr.on('data', (bytes) => (stderr += bytes)) + child.stdin.on('error', () => {}) + child.stdin.end(stringifyRecord(request)) + const timer = setTimeout(() => { + timedOut = true + try { + process.kill(-child.pid, 'SIGKILL') + } catch { + /* The bounded child may already have completed. */ + } + }, 12000) + child.on('message', (message) => { + if (message !== 'pause-ready' || pause) return + pause = { + observedReadyAt: new Date().toISOString(), + requestedMs: 250, + signal: 'SIGSTOP', + } + resumeTimer = setTimeout(() => { + try { + process.kill(child.pid, 'SIGCONT') + pause.resumedAt = new Date().toISOString() + pause.resumeSignal = 'SIGCONT' + } catch (error) { + pause.error = error.message + } + }, 250) + }) + child.on('error', reject) + child.on('close', (exitCode, signal) => { + clearTimeout(timer) + clearTimeout(resumeTimer) + const processRecord = { + startedAt, + completedAt: new Date().toISOString(), + timeoutMs: 12000, + exitCode, + signal, + timedOut, + pause, + stderr, + } + if (exitCode !== 0 || signal || timedOut) + return reject( + new Error( + `Native timing process failed: ${JSON.stringify(processRecord)}`, + ), + ) + try { + resolve({ result: parseRecord(stdout), process: processRecord }) + } catch (error) { + reject(error) + } + }) + }) +} + +if (process.argv[2] === '--worker') { + const request = parseRecord(fs.readFileSync(0, 'utf8')) + const result = observeTimingKnown(request.source, { + ...request, + stop: () => { + process.send('pause-ready') + process.kill(process.pid, 'SIGSTOP') + }, + }) + process.stdout.write(stringifyRecord(result)) + if (process.connected) process.disconnect() +} diff --git a/scripts/qualify-jsconfuser-vm/timing.mjs b/scripts/qualify-jsconfuser-vm/timing.mjs new file mode 100644 index 00000000..3c337ba0 --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/timing.mjs @@ -0,0 +1,301 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { sources, baselineOptions } from './config.mjs' +import { runWorker } from './qualify.mjs' +import { snapshot, PIN } from './audit.mjs' +import { exactEqual, stringifyRecord, parseRecord } from './codec.mjs' +import { patchAddedOpcodeCensus } from './patch.mjs' +import { + timingSourceRoles, + observeTimingKnown, + runTimingNative, +} from './timing-observation.mjs' + +export const timingSources = { + arithmetic: sources.arithmetic, + finalizers: + 'function work() { let value = 7; window.SAVED = () => value; window.pauseGate(); return value; } function parent() { try { try { return work(); } catch (error) { return -1; } } finally { window.CLEANED = true; } } window.TEST_OUTPUT = [parent(), window.CLEANED, window.SAVED()];', +} +const patch = { + selfModifying: true, + aliasedOpcodes: true, + macroOpcodes: true, + specializedOpcodes: true, + antiInstrumentation: true, +} +export const timingProfiles = [ + { name: 'baseline', options: {} }, + { name: 'timing', options: { timingChecks: 100 } }, + { name: 'patch', options: patch }, + { name: 'timingPatch', options: { ...patch, timingChecks: 100 } }, + { name: 'encoded', options: { encodeBytecode: true } }, + { + name: 'timingEncoded', + options: { encodeBytecode: true, timingChecks: 100 }, + }, + { name: 'table', options: { handlerTable: true } }, + { name: 'timingTable', options: { handlerTable: true, timingChecks: 100 } }, +] +export const timingSeeds = [231, 232, 233] +// A record keeps each process's metadata; the worker's parsed result is already in the record, and +// its raw stdout would repeat it many times over. +const withoutStreams = (process) => + Object.fromEntries( + Object.entries(process).filter( + ([key]) => !['stdout', 'stderr'].includes(key), + ), + ) +export function assertTimingBehavior(actual, expected) { + if (!exactEqual(actual, expected)) + throw new Error('Timing behavior differs from the independent oracle') +} +export async function main(args = process.argv.slice(2)) { + const output = args[0] + const smoke = args[1] === '--smoke' + if ( + !output || + (args.length !== 1 && !(args.length === 2 && smoke)) || + fs.existsSync(output) + ) + throw new Error('Use timing.mjs NEW_OUTPUT [--smoke]') + const selectedSources = smoke + ? { finalizers: timingSources.finalizers } + : timingSources + const seeds = smoke ? [timingSeeds[0]] : timingSeeds + const profiles = smoke ? timingProfiles.slice(0, 2) : timingProfiles + const before = snapshot({ generation: true }) + fs.mkdirSync(output, { recursive: true }) + const protocol = { + pin: PIN, + seeds, + sources: selectedSources, + profiles, + boundary: + 'Known-source nonminified canonical numeric runtime, numeric/base64 payloads and switch/table dispatch; all other representation options false', + clocks: { + equalStepMs: 100, + greaterStepMs: 101, + realSlowMs: 250, + realPauseMs: 250, + pause: 'worker SIGSTOP, parent SIGCONT', + }, + limitsMs: { + generation: 65000, + inspect: 20000, + nativeVM: 10000, + nativeParent: 12000, + }, + inputs: before, + } + fs.writeFileSync( + path.join(output, 'protocol.json'), + stringifyRecord(protocol), + ) + const records = [] + const worker = async (request, limit) => { + const result = await runWorker(request, limit) + if ( + !result.ok || + result.process.exitCode !== 0 || + result.process.signal || + result.process.timedOut + ) + throw new Error('Timing qualification worker failed') + return result + } + for (const [caseName, source] of Object.entries(selectedSources)) + for (const seed of seeds) + for (const profile of profiles) { + const name = `${caseName}--${profile.name}--${seed}`, + directory = path.join(output, name) + fs.mkdirSync(directory) + fs.writeFileSync(path.join(directory, 'source.js'), source) + const options = { ...baselineOptions, ...profile.options } + fs.writeFileSync( + path.join(directory, 'options.json'), + stringifyRecord(options), + ) + const journalPath = path.join(directory, 'generation.ndjson') + fs.writeFileSync(journalPath, '') + const generation = await worker( + { + action: 'generate', + source, + options, + seed, + journalPath, + identities: { java: before.java, jar: before.jar }, + }, + 65000, + ) + const encoded = generation.result.raw.replace( + /^(?:\/\/[^\n]*\r?\n)+/, + '', + ) + fs.writeFileSync(path.join(directory, 'encoded.js'), encoded) + const roles = timingSourceRoles(encoded), + active = roles.value === 100 + if ( + active !== Boolean(profile.options.timingChecks) || + roles.emitted.numericTransport !== + Boolean(profile.options.encodeBytecode) || + roles.emitted.handlerTable !== Boolean(profile.options.handlerTable) + ) + throw new Error( + `Requested timing/representation effect is absent: ${name}`, + ) + const inspection = await worker( + { action: 'inspect', source: encoded }, + 20000, + ), + current = inspection.result + // Timing checks live in the interpreter only, so every row recovers. + if ( + !current.publicRecord.ok || + !current.checks.ok || + !current.inputState + ) + throw new Error(`Recovery failed: ${name}`) + const normalSource = observeTimingKnown(source, { clock: () => 0 }) + assertTimingBehavior(normalSource.behavior, { + kind: 'value', + value: caseName === 'finalizers' ? [7, true, 7] : 0, + }) + const equal = observeTimingKnown(encoded, { + vmName: roles.vmName, + clock: (index) => index * 100, + }) + assertTimingBehavior(equal.behavior, normalSource.behavior) + const greater = observeTimingKnown(encoded, { + vmName: roles.vmName, + clock: (index) => (index === 0 ? 0 : 101), + }) + if ( + active && + (greater.behavior.kind !== 'error' || + !greater.state.wordsZero || + !greater.state.currentFramePayloadCleared) + ) + throw new Error('Greater-than poisoning oracle did not fire') + if (!active) + assertTimingBehavior(greater.behavior, normalSource.behavior) + const observations = { equal, greater } + const processes = [generation.process, inspection.process] + for (const mode of caseName === 'finalizers' + ? ['fast', 'slow', 'paused'] + : ['fast']) { + const sourceRun = await runTimingNative({ source, mode }) + const encodedRun = await runTimingNative({ + source: encoded, + mode, + vmName: roles.vmName, + }) + processes.push(sourceRun.process, encodedRun.process) + const decodedRun = await runTimingNative({ + source: current.publicRecord.output, + mode, + }) + processes.push(decodedRun.process) + assertTimingBehavior( + decodedRun.result.behavior, + sourceRun.result.behavior, + ) + if (active && mode !== 'fast') { + const result = encodedRun.result + if ( + result.behavior.kind !== 'error' || + !result.state.wordsZero || + result.state.cleaned !== undefined || + !result.state.captures.some( + (capture) => !capture.closed && capture.backing === undefined, + ) || + result.blockedMs <= 100 + ) + throw new Error( + `Active timing cleanup/state oracle failed: ${name}/${mode}`, + ) + } else + assertTimingBehavior( + encodedRun.result.behavior, + sourceRun.result.behavior, + ) + if ( + mode === 'paused' && + [sourceRun, encodedRun, decodedRun].some( + (run) => run.process.pause?.resumeSignal !== 'SIGCONT', + ) + ) + throw new Error('Pause completion is missing') + observations[mode] = { + source: sourceRun.result, + encoded: encodedRun.result, + decoded: decodedRun.result, + } + } + fs.writeFileSync( + path.join(directory, 'decoded.js'), + current.publicRecord.output, + ) + const record = { + name, + caseName, + seed, + profile, + timing: roles.value, + emitted: roles.emitted, + active, + publicDiagnostic: current.publicRecord.diagnostic, + firstDiagnostic: current.container.diagnostic, + recoveryQualified: true, + inputState: current.inputState, + checks: current.checks, + census: patchAddedOpcodeCensus(roles.disabled), + observations, + processes: processes.map(withoutStreams), + } + fs.writeFileSync( + path.join(directory, 'record.json'), + stringifyRecord(record), + ) + records.push(record) + process.stdout.write( + `${name}: recovered${active ? '; trigger observed' : ''}\n`, + ) + } + if (!exactEqual(before, snapshot({ generation: true }))) + throw new Error('Execution inputs changed during timing qualification') + // Read the bytes back before publication; stdout alone cannot qualify a run. + for (const record of records) { + const reread = parseRecord( + fs.readFileSync(path.join(output, record.name, 'record.json'), 'utf8'), + ) + if (!exactEqual(reread, record)) + throw new Error('Timing cell readback changed') + } + fs.writeFileSync( + path.join(output, 'summary.json'), + stringifyRecord({ + protocol, + records, + inputsUnchanged: true, + readback: true, + }), + ) + if ( + !exactEqual( + parseRecord(fs.readFileSync(path.join(output, 'summary.json'), 'utf8')) + .records, + records, + ) + ) + throw new Error('Timing publication readback changed') +} +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) + main().catch((error) => { + process.stderr.write(`${error.stack}\n`) + process.exitCode = 1 + }) diff --git a/scripts/qualify-jsconfuser-vm/worker.mjs b/scripts/qualify-jsconfuser-vm/worker.mjs new file mode 100644 index 00000000..1f7337ad --- /dev/null +++ b/scripts/qualify-jsconfuser-vm/worker.mjs @@ -0,0 +1,96 @@ +import fs from "node:fs"; +import { runInNewContext } from "node:vm"; +import { performance } from "node:perf_hooks"; +import { + parseRecord, + tagged, + stringifyRecord, +} from "./codec.mjs"; +import { inspectCurrent } from "./oracles.mjs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { javaRoot, encoderRoot } from "./audit.mjs"; +import { generationSession } from "./generation.mjs"; + +export function observeKnown(source) { + const context = { TEST_OUTPUT: undefined, performance, atob, btoa }; + context.window = context; + context.self = context; + try { + runInNewContext(source, context, { timeout: 10000 }); + return { kind: "value", value: context.TEST_OUTPUT }; + } catch (error) { + return { + kind: error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT" ? "timeout" : "error", + name: error.name, + message: error.message, + }; + } +} +async function generate(request) { + const emit = (event) => + fs.appendFileSync( + request.journalPath, + JSON.stringify({ + eventAt: new Date().toISOString(), + value: tagged(event), + }) + "\n", + ); + return generationSession( + { ...request, javaRoot }, + { + emit, + loadCompiler: async () => + ( + await import( + pathToFileURL( + path.join( + encoderRoot, + "node_modules/google-closure-compiler/lib/node/index.js", + ), + ).href + ) + ).default, + loadEncoder: async () => + ( + await import( + pathToFileURL(path.join(encoderRoot, "src/index.ts")).href + ) + ).JsConfuserVM, + }, + ); +} +if (process.argv[2] === "--worker") { + try { + const request = parseRecord(fs.readFileSync(0, "utf8")); + let result; + if (request.action === "generate") result = await generate(request); + else if (request.action === "inspect") + result = inspectCurrent(request.source); + else if (request.action === "behavior") + result = observeKnown(request.source); + else if (request.action === "patch-inspect") + result = { + inspection: inspectCurrent(request.source), + ...(await import("./patch.mjs")).patchCensusOf( + request.source, + request.selfModifying, + ), + }; + else throw new Error("Unknown qualification worker action"); + process.stdout.write(stringifyRecord({ ok: true, result })); + } catch (error) { + process.stdout.write( + stringifyRecord({ + ok: false, + error: { + name: error.name, + message: error.message, + cause: error.cause?.message, + provenance: error.provenance, + }, + }), + ); + process.exitCode = 1; + } +} diff --git a/test/scripts/qualify-jsconfuser-vm/frontend.test.js b/test/scripts/qualify-jsconfuser-vm/frontend.test.js new file mode 100644 index 00000000..b3f0c2f2 --- /dev/null +++ b/test/scripts/qualify-jsconfuser-vm/frontend.test.js @@ -0,0 +1,160 @@ +import fs from "node:fs"; +import assert from "node:assert/strict"; +import { test } from "vitest"; +import { parse } from "../../../node_modules/@babel/parser/lib/index.js"; +import generate from "../../../node_modules/@babel/generator/lib/index.js"; +import traverse from "../../../node_modules/@babel/traverse/lib/index.js"; +import { makeContext } from "../../../src/vm/jsconfuser-vm/container-role-helpers.js"; +import { emittedOptionEffect } from "../../../scripts/qualify-jsconfuser-vm/effects.mjs"; +import { + diagnoseRuntimeLayout, + runtimeLayout, + classLayoutEffect, +} from "../../../scripts/qualify-jsconfuser-vm/layout.mjs"; +const directory = new URL( + "../../../test/vm/jsconfuser-vm/fixtures/minified-class-frontend/", + import.meta.url, +); +const source = (profile, seed = 172) => + fs.readFileSync( + new URL(`arithmetic--${profile}--${seed}.js`, directory), + "utf8", + ); +for (const seed of [172, 173, 174]) + test(`${seed}: every enabled component has its frozen removed-option control`, () => { + const peers = new Map( + ["baseline", "minify", "classObfuscation", "minifyClassObfuscation"].map( + (profile) => [profile, source(profile, seed)], + ), + ); + for (const [profile, encoded] of peers) { + assert.ok(runtimeLayout(encoded), profile); + const effect = emittedOptionEffect( + profile, + peers.get("baseline"), + encoded, + peers, + ); + assert.equal( + effect.observed, + profile === "baseline" ? null : true, + profile, + ); + if (profile === "minifyClassObfuscation") + assert.deepEqual(effect.components, { + minify: true, + classObfuscation: true, + }); + } + assert.equal( + classLayoutEffect(peers.get("baseline"), peers.get("classObfuscation")) + .witness, + "payload-role-permutation", + ); + assert.equal( + classLayoutEffect(peers.get("baseline"), peers.get("baseline")).observed, + false, + ); + assert.equal( + emittedOptionEffect( + "minifyClassObfuscation", + peers.get("baseline"), + peers.get("minifyClassObfuscation"), + new Map(), + ).observed, + null, + ); + }); +function setup(profile = "baseline") { + const ast = parse(source(profile)), + context = makeContext(ast); + const call = context + .nodes(context.program) + .find( + ({ node }) => + node.type === "CallExpression" && + node.arguments.length === 1 && + context.bindingFor(node.callee)?.path.node.body?.body[0]?.type === + "IfStatement" && + context.paths.get(node).parentPath.node.type === "NewExpression", + ).node; + const decoder = context.bindingFor(call.callee).path.node; + return { ast, context, call, decoder }; +} +function refusal(change, code, profile = "baseline") { + const state = setup(profile); + change(state); + const encoded = generate(state.ast).code; + assert.equal(runtimeLayout(encoded), null); + const result = diagnoseRuntimeLayout(encoded); + assert.equal(result.ok, false); + assert.equal(result.diagnostic.code, code); + assert.equal( + classLayoutEffect(source("baseline"), encoded).witness, + "unconnected-layout", + ); +} +test("numeric transport alpha follows the exact decoder/helper/mode/payload bindings", () => { + const state = setup(); + let count = 0; + traverse(state.ast, { + Scopable(path) { + for (const name of Object.keys(path.scope.bindings)) + path.scope.rename(name, `alpha_${count++}`); + }, + }); + assert.deepEqual( + runtimeLayout(generate(state.ast).code), + runtimeLayout(source("baseline")), + ); +}); +test("numeric transport body cannot become arbitrary array pass-through", () => + refusal(({ decoder }) => { + decoder.body.body.splice(1); + }, "numeric-transport-body")); +test("numeric transport bound mode must remain false", () => + refusal(({ context, decoder }) => { + context.bindingFor( + decoder.body.body[0].test.argument, + ).path.node.init.value = true; + }, "numeric-transport-mode")); +test("numeric transport inlined mode cannot become encoded mode", () => + refusal( + ({ decoder }) => { + decoder.body.body[0].test.argument.value = true; + }, + "numeric-transport-body", + "classObfuscation", + )); +test("numeric transport decoder cannot be overwritten", () => + refusal(({ ast, decoder }) => { + ast.program.body.unshift( + ...parse(`${decoder.id.name}=unprovedDecoder;`).program.body, + ); + }, "numeric-transport-binding")); +test("numeric transport helper body must stay complete", () => + refusal(({ context, decoder }) => { + const helper = context.bindingFor( + decoder.body.body[1].declarations[0].init.callee, + ).path.node; + helper.body.body.push( + ...parse("globalThis.unprovedEffect();").program.body, + ); + }, "numeric-transport-helper")); +test("numeric transport payload cannot escape or be mutated through a second reference", () => + refusal(({ ast, call }) => { + assert.equal(call.arguments[0].type, "Identifier"); + ast.program.body.unshift( + ...parse(`${call.arguments[0].name}[0]=99;`).program.body, + ); + }, "numeric-transport-payload")); +test("numeric transport argument evaluation cannot contain an extra effect", () => + refusal(({ call }) => { + call.arguments[0] = { + type: "SequenceExpression", + expressions: [ + parse("unprovedEffect();").program.body[0].expression, + call.arguments[0], + ], + }; + }, "numeric-transport-payload")); diff --git a/test/scripts/qualify-jsconfuser-vm/oracles.test.js b/test/scripts/qualify-jsconfuser-vm/oracles.test.js new file mode 100644 index 00000000..db02dc80 --- /dev/null +++ b/test/scripts/qualify-jsconfuser-vm/oracles.test.js @@ -0,0 +1,62 @@ +// Pure codec, oracle and replay-output challenges for the qualifier. No target or encoder executes. +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { test } from "vitest"; +import { + exactEqual, + parseRecord, + stringifyRecord, +} from "../../../scripts/qualify-jsconfuser-vm/codec.mjs"; +import { + inspectCurrent, + outputChecks, +} from "../../../scripts/qualify-jsconfuser-vm/oracles.mjs"; +import { replayOutputAllowed } from "../../../scripts/qualify-jsconfuser-vm/qualify.mjs"; + +test("lossless codec preserves nonenumerable own string keys", () => { + const value = Object.defineProperty({}, "x", { value: undefined }); + assert.equal(exactEqual(value, {}), false); + assert.deepEqual( + Object.getOwnPropertyNames(parseRecord(stringifyRecord(value))), + ["x"], + ); +}); + +test("output oracle refuses renamed numeric dispatcher passthrough", () => { + const fixture = fs.readFileSync( + new URL( + "../../../test/vm/jsconfuser-vm/fixtures/minified-class-boot/encoded-169.js", + import.meta.url, + ), + "utf8", + ); + const emitted = inspectCurrent(fixture).publicRecord.result; + const dispatcher = + "function d(w){let p=0,r=[];while(p { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "vm-admission-review-")); + try { + const frozen = path.join(root, "frozen"); + fs.mkdirSync(frozen); + fs.symlinkSync(frozen, path.join(root, "alias")); + assert.throws( + () => replayOutputAllowed(frozen, path.join(root, "alias", "revision")), + /outside/, + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/test/scripts/qualify-jsconfuser-vm/qualify.test.js b/test/scripts/qualify-jsconfuser-vm/qualify.test.js new file mode 100644 index 00000000..a8838f1d --- /dev/null +++ b/test/scripts/qualify-jsconfuser-vm/qualify.test.js @@ -0,0 +1,848 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import { test } from "vitest"; +import { + emittedOptionEffect, + eligibleEffectControl, + assertStableExecutionInputs, + parseArgs, + runWorker, +} from "../../../scripts/qualify-jsconfuser-vm/qualify.mjs"; +import { generationSession } from "../../../scripts/qualify-jsconfuser-vm/generation.mjs"; +import { profiles } from "../../../scripts/qualify-jsconfuser-vm/config.mjs"; +import { EventEmitter } from "node:events"; +import { PassThrough } from "node:stream"; + +const fixture = (relative) => + fs.readFileSync( + new URL( + `../../../test/vm/jsconfuser-vm/fixtures/${relative}/encoded.js`, + import.meta.url, + ), + "utf8", + ); + +test("compound effect without component controls is unavailable", () => { + const effect = emittedOptionEffect( + "stringConcealingControlFlowFlattening", + "var BYTECODE = [1];", + "var BYTECODE = [2];", + new Map(), + ); + assert.equal(effect.observed, null); + assert.deepEqual(effect.components, { + stringConcealing: null, + controlFlowFlattening: null, + }); +}); + +test("an observed absent component fails a compound effect despite a missing control", () => { + const effect = emittedOptionEffect( + "minifyEncodeBytecode", + "baseline", + "candidate", + new Map([["minify", "control"]]), + ); + assert.equal(effect.components.minify, null); + assert.equal(effect.components.encodeBytecode, false); + assert.equal(effect.observed, false); +}); + +test("self-modifying controls distinguish proved absence from an observer decline", () => { + const candidate = fixture("self-modifying-boot"); + const noPatch = fixture("corpus/raw/f-literals-order"); + const unsupported = fixture("anti-runtime-boot"); + const effect = (control) => + emittedOptionEffect( + "minifySelfModifying", + noPatch, + candidate, + new Map([ + ["minify", control], + ["selfModifying", `${candidate} `], + ]), + ); + assert.equal(effect(noPatch).components.selfModifying, true); + assert.equal(effect(unsupported).components.selfModifying, null); + assert.equal(effect(unsupported).observed, null); +}); + +test("compound effects require every observed component", () => { + const effect = emittedOptionEffect( + "minifyEncodeBytecode", + "baseline", + "new Uint32Array(1)", + new Map([ + ["encodeBytecode", "a".repeat(100)], + ["minify", "plain runtime"], + ]), + ); + assert.deepEqual(effect.components, { minify: true, encodeBytecode: true }); + assert.equal(effect.observed, true); +}); + +test("encoder failures cannot supply representation controls", () => { + for (const status of ["encoder-mismatch", "generation-error"]) + assert.equal(eligibleEffectControl({ encoded: true, status }), false); + assert.equal(eligibleEffectControl({ status: "accepted" }), false); + for (const status of ["accepted", "declined", "decoder-mismatch"]) + assert.equal(eligibleEffectControl({ encoded: true, status }), true); +}); + +test("publication refuses changed bytes, revisions, pin or relevant worktree state", () => { + const before = { + encoderCommit: "encoder", + decoderCommit: "decoder", + decoderSourceTree: "tree", + encoderWorktreeStatus: "", + decoderWorktreeStatus: " M src/main.js", + decoderUncommittedSha256: "delta", + tools: { commit: "decoder", status: "", uncommittedSha256: null }, + }; + assert.doesNotThrow(() => + assertStableExecutionInputs(before, structuredClone(before)), + ); + for (const key of Object.keys(before)) { + const after = structuredClone(before); + after[key] = "changed"; + assert.throws( + () => assertStableExecutionInputs(before, after), + /inputs or repository state changed/, + ); + } +}); + +import { + tagged, + exactEqual, + stringifyRecord, + parseRecord, +} from "../../../scripts/qualify-jsconfuser-vm/codec.mjs"; +import { + classLayoutEffect, + runtimeLayout, +} from "../../../scripts/qualify-jsconfuser-vm/layout.mjs"; +import { + inspectCurrent, + outputChecks, + assessCurrent, +} from "../../../scripts/qualify-jsconfuser-vm/oracles.mjs"; +import { replayOutputAllowed, verifyFrozen } from "../../../scripts/qualify-jsconfuser-vm/qualify.mjs"; +import path from "node:path"; +import os from "node:os"; +import { + manifest, + auditEntries, + decoderRoot, +} from "../../../scripts/qualify-jsconfuser-vm/audit.mjs"; +import { parse } from "../../../node_modules/@babel/parser/lib/index.js"; +import { + cellSummary, + finalizeCells, + assertRecoveryBoundary, + recoveryRows, + verifyRawPopulation, + assertStoredDisposition, +} from "../../../scripts/qualify-jsconfuser-vm/finalize.mjs"; +import { sha256 } from "../../../scripts/qualify-jsconfuser-vm/audit.mjs"; +import traverse from "../../../node_modules/@babel/traverse/lib/index.js"; +import generate from "../../../node_modules/@babel/generator/lib/index.js"; + +const read = (relative) => + fs.readFileSync( + new URL( + `../../../test/vm/jsconfuser-vm/fixtures/${relative}`, + import.meta.url, + ), + "utf8", + ); +test("lossless analysis codec separates undefined/null, holes and numeric exceptional values", () => { + for (const [left, right] of [ + [[undefined], [null]], + [{ x: undefined }, {}], + [-0, 0], + [NaN, null], + [new Array(1), [undefined]], + ]) + assert.equal(exactEqual(left, right), false); + const value = { + pool: [undefined, null, NaN, -0, Infinity, -Infinity], + map: new Map([[undefined, 7n]]), + set: new Set([null, undefined]), + hole: new Array(1), + tagShape: ["undefined"], + }; + assert.equal(exactEqual(value, parseRecord(stringifyRecord(value))), true); + assert.deepEqual(tagged(value.pool)[2][0][1], ["undefined"]); + assert.throws(() => tagged(new Date()), /Opaque/); + const cyclic = {}; + cyclic.self = cyclic; + assert.throws(() => tagged(cyclic), /Cyclic/); +}); +test("minified class has a binding-connected class witness independent of minify compression", () => { + const plain = read("minified-runtime-boot/encoded.js"); + assert.equal(runtimeLayout(plain).optionalSlotGuards, 9); + for (const seed of [169, 170, 171]) { + const candidate = read(`minified-class-boot/encoded-${seed}.js`); + const witness = classLayoutEffect(plain, candidate); + assert.equal(witness.observed, true); + assert.equal(witness.witness, "connected-optional-slots-erased"); + assert.equal(witness.candidate.optionalSlotGuards, 0); + const effect = emittedOptionEffect( + "minifyClassObfuscation", + plain, + candidate, + new Map([ + ["minify", plain], + ["classObfuscation", candidate + " ".repeat(10000)], + ]), + ); + assert.deepEqual(effect.components, { + minify: true, + classObfuscation: true, + }); + assert.equal( + effect.classWitness.witness, + "connected-optional-slots-erased", + ); + assert.equal(effect.observed, true); + assert.equal(classLayoutEffect(candidate, candidate).observed, false); + assert.equal( + classLayoutEffect("unrecognized source", candidate).observed, + null, + ); + assert.equal( + emittedOptionEffect("minifyClassObfuscation", plain, candidate, new Map()) + .observed, + null, + ); + } +}); +test("compression cannot supply the missing or absent class component", () => { + const plain = read("minified-runtime-boot/encoded.js"); + const effect = emittedOptionEffect( + "minifyClassObfuscation", + plain, + plain, + new Map([ + ["minify", plain], + ["classObfuscation", plain + " "], + ]), + ); + assert.equal(effect.components.minify, true); + assert.equal(effect.components.classObfuscation, false); + assert.equal(effect.observed, false); +}); +test("class effect follows binding alpha and refuses disconnected payloads", () => { + const plain = read("minified-runtime-boot/encoded.js"), + candidate = read("minified-class-boot/encoded-169.js"); + const ast = parse(candidate); + let index = 0; + traverse(ast, { + Scopable(path) { + for (const name of Object.keys(path.scope.bindings)) + path.scope.rename(name, `alpha_${index++}`); + }, + }); + assert.deepEqual(runtimeLayout(generate(ast).code), runtimeLayout(candidate)); + assert.equal(classLayoutEffect(plain, generate(ast).code).observed, true); + // Removing the actual boot call leaves a real payload/constructor but severs + // its dispatch receiver connection. A disconnected shape supplies no witness. + const disconnected = parse(candidate); + disconnected.program.body.pop(); + assert.equal(runtimeLayout(generate(disconnected).code), null); + assert.equal( + classLayoutEffect(plain, generate(disconnected).code).observed, + null, + ); + const removed = emittedOptionEffect( + "minifyClassObfuscation", + plain, + plain, + new Map([["classObfuscation", plain + " "]]), + ); + assert.equal(removed.components.minify, true); + assert.equal(removed.components.classObfuscation, null); + assert.equal(removed.observed, null); +}); +test("native test bodies stay outside the parser-only closure audit", () => { + for (const file of [ + "runtime-binding-identities.test.js", + "owned-chain-source.test.js", + ]) + assert.throws( + () => + auditEntries([path.join(decoderRoot, "test/vm/jsconfuser-vm", file)]), + /Unreviewed import|Execution surface/, + ); +}); +test("supplied current observations are reused losslessly and expected undefined is an oracle", async () => { + const inspected = inspectCurrent(read("minified-class-boot/encoded-169.js")); + let count = 0; + const result = await assessCurrent( + inspected, + "source", + { + encoded: "encoded", + sourceObservation: { kind: "value", value: undefined }, + encodedObservation: { kind: "value", value: undefined }, + value: undefined, + }, + () => { + count++; + return { kind: "value", value: null }; + }, + ); + assert.equal(count, 1); + assert.equal(result.status, "behavior-mismatch"); + assert.equal(result.qualified, false); +}); +test("current public parser replay never credits historical good output as native behavior", async () => { + const encoded = read("minified-class-boot/encoded-169.js"); + const inspected = inspectCurrent(encoded); + assert.equal(inspected.publicRecord.ok, true); + assert.equal(inspected.checks.ok, true); + assert.equal(inspected.inputState, true); + assert.equal(inspected.projection.constants.includes(undefined), true); + assert.equal( + exactEqual(inspected, parseRecord(stringifyRecord(inspected))), + true, + ); + const parser = await assessCurrent( + inspected, + read("minified-class-boot/source.js"), + { encoded }, + null, + ); + assert.equal(parser.status, "parser-only"); + assert.equal(parser.qualified, false); + const decline = inspectCurrent(encoded, (source) => ({ + ok: false, + result: null, + output: source, + diagnostic: { code: "deliberate-decline" }, + })); + const refused = await assessCurrent(decline, "source", { encoded }, () => { + throw new Error("Decline must not execute any output"); + }); + assert.equal(refused.status, "declined"); + assert.equal(refused.qualified, false); +}); +test("replay retains frozen manifests and refuses output paths inside them", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "vm-qualification-test-")); + try { + fs.writeFileSync(path.join(root, "source.js"), "source"); + const expected = manifest(root); + verifyFrozen(root, expected); + assert.throws( + () => replayOutputAllowed(root, path.join(root, "revision")), + /outside/, + ); + fs.writeFileSync(path.join(root, "source.js"), "changed"); + assert.throws(() => verifyFrozen(root, expected), /changed/); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("codec retains all own data keys and collection extras without invoking accessors", () => { + for (const value of [ + Object.defineProperty({}, "x", { value: undefined }), + Object.assign(new Map([[1, undefined]]), { extra: -0 }), + Object.assign(new Set([NaN]), { extra: null }), + Object.assign([undefined], { extra: Infinity }), + ]) { + const restored = parseRecord(stringifyRecord(value)); + assert.equal(exactEqual(value, restored), true); + assert.deepEqual( + Object.getOwnPropertyNames(restored), + Object.getOwnPropertyNames(value), + ); + for (const key of Object.getOwnPropertyNames(value)) + assert.deepEqual( + Object.getOwnPropertyDescriptor(restored, key), + Object.getOwnPropertyDescriptor(value, key), + ); + } + let reads = 0; + const accessor = Object.defineProperty({}, "x", { + get() { + reads++; + return 1; + }, + }); + assert.throws(() => stringifyRecord(accessor), /Accessor/); + assert.equal(reads, 0); + assert.throws( + () => tagged(Object.assign(new Map(), { [Symbol("x")]: 1 })), + /Symbol/, + ); +}); +test("general argument defaults and every existing profile remain available without generation", () => { + const defaults = parseArgs(["--output", "unused"]); + assert.deepEqual(defaults.seeds, [1, 2, 3]); + assert.deepEqual(defaults.profiles, Object.keys(profiles)); + assert.deepEqual( + parseArgs(["--output", "unused", "--profiles", "all"]).profiles, + Object.keys(profiles), + ); + for (const profile of Object.keys(profiles)) + assert.deepEqual( + parseArgs(["--output", "unused", "--profiles", profile]).profiles, + [profile], + ); + assert.deepEqual( + parseArgs([ + "--output", + "unused", + "--profiles", + "baseline,minify,classObfuscation,minifyClassObfuscation", + "--seeds", + "172,173,174", + ]).seeds, + [172, 173, 174], + ); +}); +test("actual output structure rejects surviving dispatch and fallback while keeping user switches", () => { + const input = read("minified-class-boot/encoded-169.js"), + current = inspectCurrent(input).publicRecord.result; + for (const suffix of [ + "function d(w){let p=0;while(p { + for (const timeout of [false, true]) { + const events = []; + let watchdog; + class FakeCompiler { + constructor() { + this.javaPath = "identified-java"; + this.commandArguments = ["-jar", "identified-jar"]; + } + run(callback) { + const child = new EventEmitter(); + child.pid = 42; + child.stdout = new PassThrough(); + child.stderr = new PassThrough(); + child.kill = (signal) => { + child.emit("close", null, signal); + }; + child.once("close", (code) => + callback(code, "partial stdout", "partial stderr"), + ); + queueMicrotask(() => { + child.stdout.write("partial stdout"); + child.stderr.write("partial stderr"); + if (timeout) watchdog(); + else child.emit("close", 7, null); + }); + return child; + } + } + const original = FakeCompiler.prototype.run; + const encoder = { + obfuscate: async () => { + Math.random(); + return new Promise((resolve, reject) => + new FakeCompiler().run(() => + reject(new Error("trusted compiler failure")), + ), + ); + }, + }; + let error; + try { + await generationSession( + { + source: "data only", + options: { minify: true }, + seed: 172, + javaRoot: "/identified-jdk", + identities: { java: "identified-java", jar: "identified-jar" }, + }, + { + emit: (event) => events.push(structuredClone(event)), + loadCompiler: async () => FakeCompiler, + loadEncoder: async () => encoder, + setTimer: (callback) => { + watchdog = callback; + return 1; + }, + clearTimer: () => {}, + }, + ); + } catch (caught) { + error = caught; + } + assert.ok(error); + assert.equal(error.provenance.rng.draws, 1); + assert.equal(FakeCompiler.prototype.run, original); + const finish = events.find( + (event) => event.event === "closure-finish", + ).process; + assert.deepEqual(finish.argv, ["-jar", "identified-jar"]); + assert.equal(finish.stdout, "partial stdout"); + assert.equal(finish.stderr, "partial stderr"); + assert.equal(finish.timedOut, timeout); + assert.equal(finish.signal, timeout ? "SIGKILL" : null); + assert.equal(finish.exitCode, timeout ? null : 7); + assert.ok(events.some((event) => event.event === "rng-draw")); + assert.ok(events.some((event) => event.event === "generation-error")); + } +}); +test("trusted parent watchdog preserves durable partial generation journal and signal", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "vm-journal-control-")); + const journalPath = path.join(root, "journal.ndjson"); + fs.writeFileSync(journalPath, ""); + try { + let watchdog, killed, spawned; + const spawnWorker = () => { + const child = new EventEmitter(); + spawned = child; + child.pid = 43; + child.stdin = new PassThrough(); + child.stdout = new PassThrough(); + child.stderr = new PassThrough(); + child.stdin.once("finish", () => { + fs.appendFileSync(journalPath, '{"event":"rng-draw","draws":3}\n'); + child.stdout.write("partial record"); + child.stderr.write("partial diagnostic"); + watchdog(); + }); + return child; + }; + const result = await runWorker({ action: "generate", journalPath }, 65000, { + spawnWorker, + killGroup: (pid) => { + killed = pid; + queueMicrotask(() => spawned.emit("close", null, "SIGKILL")); + }, + setTimer: (callback) => { + watchdog = callback; + return 1; + }, + clearTimer: () => {}, + }); + assert.equal(result.ok, false); + assert.equal(killed, 43); + assert.equal(result.process.timedOut, true); + assert.equal(result.process.signal, "SIGKILL"); + assert.match(result.process.journal, /draws/); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("pre-import generation failure retains identities and zero-draw provenance", async () => { + const events = []; + let error; + try { + await generationSession( + { + seed: 174, + javaRoot: "/identified-jdk", + identities: { java: "java-sha", jar: "jar-sha" }, + }, + { + emit: (event) => events.push(event), + loadCompiler: async () => { + throw new Error("trusted import refusal"); + }, + loadEncoder: async () => { + throw new Error("Must not reach encoder"); + }, + }, + ); + } catch (caught) { + error = caught; + } + assert.equal(error.message, "trusted import refusal"); + assert.equal(error.provenance.rng.draws, 0); + assert.deepEqual(error.provenance.processes, []); + assert.deepEqual(events[0].identities, { java: "java-sha", jar: "jar-sha" }); + assert.equal(events.at(-1).event, "generation-error"); +}); +test("trusted worker spawn error retains close disposition rather than an early partial record", async () => { + const spawnWorker = () => { + const child = new EventEmitter(); + child.stdin = new PassThrough(); + child.stdout = new PassThrough(); + child.stderr = new PassThrough(); + queueMicrotask(() => { + child.emit("error", new Error("trusted spawn refusal")); + child.emit("close", -2, null); + }); + return child; + }; + const result = await runWorker({ action: "inspect" }, 20000, { + spawnWorker, + setTimer: () => 1, + clearTimer: () => {}, + }); + assert.equal(result.ok, false); + assert.equal(result.process.exitCode, -2); + assert.equal(result.process.signal, null); + assert.equal(result.process.timedOut, false); + assert.equal(result.error.message, "trusted spawn refusal"); + assert.equal(result.process.stdout, ""); + assert.equal(result.process.stderr, ""); +}); + +test("bounded finalization publishes references without retaining inspection or process records", () => { + const rows = []; + for (let caseIndex = 0; caseIndex < 14; caseIndex++) + for (const seed of [172, 173, 174]) + for (const profile of [ + "baseline", + "minify", + "classObfuscation", + "minifyClassObfuscation", + ]) + rows.push({ + id: `${caseIndex}--${profile}--${seed}`, + case: String(caseIndex), + profile, + seed, + options: { profile }, + status: "accepted", + qualified: true, + encoded: true, + }); + let reads = 0, + writes = 0; + const completed = finalizeCells(rows, { + readCell: (row) => { + reads++; + return { + ...row, + inspection: { + projection: { constants: [undefined, null, NaN, -0] }, + analysis: "large analysis".repeat(10000), + }, + processes: [{ stdout: "large output".repeat(10000) }], + }; + }, + readEncoded: (row) => (row.profile === "baseline" ? "x".repeat(1000) : "x"), + eligible: eligibleEffectControl, + writeCell: (row, record) => { + writes++; + assert.equal(record.normalizedAgreement, true); + assert.equal( + record.qualified, + ["baseline", "minify"].includes(row.profile), + ); + return { result: { path: `${row.id}/result.json`, sha256: "retained" } }; + }, + }); + assert.equal(writes, 168); + assert.equal(reads, 210); + assert.ok(stringifyRecord(completed).length < 1000000); + assert.equal(JSON.stringify(completed).includes("large analysis"), false); + assert.equal(JSON.stringify(completed).includes("large output"), false); + assert.deepEqual( + completed.map((row) => row.id), + rows.map((row) => row.id), + ); +}); + +test("finalization rejects changed cell identities and distinguishes exceptional pool values", () => { + const baseline = { + id: "b", + case: "c", + seed: 1, + profile: "baseline", + qualified: true, + }; + const candidate = { ...baseline, id: "m", profile: "minify" }; + const run = (value, changed = false) => + finalizeCells([baseline, candidate], { + readCell: (row) => ({ + ...row, + ...(changed && row === candidate ? { seed: 2 } : {}), + inspection: { + projection: { + constants: [row === baseline ? undefined : value, -0, NaN], + }, + }, + }), + readEncoded: () => "data", + eligible: () => false, + writeCell: () => ({}), + }); + assert.equal(run(undefined)[1].normalizedAgreement, true); + assert.equal(run(null)[1].normalizedAgreement, false); + assert.throws(() => run(undefined, true), /metadata changed/); + assert.throws(() => finalizeCells([baseline, baseline], {}), /Duplicate/); + const current = run(undefined)[0]; + assert.doesNotThrow(() => + assertStoredDisposition(current, structuredClone(current)), + ); + for (const change of [ + { qualified: false }, + { status: "declined" }, + { normalizedAgreement: null }, + ]) + assert.throws( + () => assertStoredDisposition(current, { ...current, ...change }), + /differs from original/, + ); +}); + +test("recovery requires the exact aborted-run manifest and unchanged mechanism boundary", () => { + const original = { + decoderCommit: "decoder", + decoderSourceTree: "tree", + node: "node", + tools: { commit: "decoder", status: "", uncommittedSha256: null }, + }; + const raw = stringifyRecord([{ path: "cell/result.json", sha256: "raw" }]); + const preparation = { + originalExitCode: 134, + originalPublication: "absent", + identical: true, + before: original, + current: structuredClone(original), + rawManifestSha256: sha256(raw), + }; + const current = structuredClone(original); + // A committed recovery-tool fix moves the decoder commit with the tools commit. + current.decoderCommit = "recovery"; + current.tools = { + commit: "recovery", + status: " M scripts/qualify-jsconfuser-vm/finalize.mjs", + uncommittedSha256: "delta", + }; + const recoveryTools = [ + "scripts/qualify-jsconfuser-vm/finalize.mjs", + "scripts/qualify-jsconfuser-vm/qualify.mjs", + ]; + assert.doesNotThrow(() => + assertRecoveryBoundary(original, preparation, current, raw, recoveryTools), + ); + for (const [mutate, changed] of [ + [(value) => (value.decoderSourceTree = "changed"), recoveryTools], + [(value) => (value.node = "changed"), recoveryTools], + [() => {}, [...recoveryTools, "scripts/qualify-jsconfuser-vm/oracles.mjs"]], + ]) { + const wrong = structuredClone(current); + mutate(wrong); + assert.throws( + () => assertRecoveryBoundary(original, preparation, wrong, raw, changed), + /boundary changed|unrelated tool/, + ); + } + const dirty = { ...original, tools: { ...original.tools, status: " M x" } }; + assert.throws( + () => + assertRecoveryBoundary( + dirty, + { ...preparation, before: dirty, current: dirty }, + current, + raw, + [], + ), + /original tools committed/, + ); + assert.throws( + () => assertRecoveryBoundary(original, preparation, current, raw + " ", []), + /preparation/, + ); + assert.throws( + () => + assertRecoveryBoundary( + original, + { ...preparation, originalExitCode: 0 }, + current, + raw, + [], + ), + /preparation/, + ); + assert.throws( + () => parseArgs(["--recover", "raw", "--output", "new"]), + /requires frozen/, + ); + assert.throws( + () => parseArgs(["--output", "new", "--raw-manifest", "m"]), + /requires --recover/, + ); +}); + +test("synthetic recovery census rejects incomplete, changed and excess raw cells", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "vm-recovery-control-")); + try { + const id = "c--baseline--172", + directory = path.join(root, id); + fs.mkdirSync(directory); + fs.writeFileSync(path.join(directory, "source.js"), "source"); + const row = { + id, + case: "c", + profile: "baseline", + seed: 172, + options: {}, + status: "declined", + qualified: false, + }; + for (const name of ["result.json", "qualification.json"]) + fs.writeFileSync(path.join(directory, name), stringifyRecord(row)); + const population = { + sources: { c: "source" }, + profiles: [{ name: "baseline", options: {} }], + seeds: [172], + }; + const expected = manifest(root); + verifyRawPopulation(root, expected); + assert.deepEqual(recoveryRows(root, population, expected), [ + cellSummary(row), + ]); + assert.throws( + () => + recoveryRows( + root, + population, + expected.filter( + (member) => !member.path.endsWith("qualification.json"), + ), + ), + /Incomplete/, + ); + assert.throws( + () => recoveryRows(root, { ...population, seeds: [173] }, expected), + /Incomplete/, + ); + assert.throws( + () => + recoveryRows(root, population, [ + ...expected, + { path: "extra/result.json" }, + ]), + /census/, + ); + fs.writeFileSync(path.join(directory, "source.js"), "changed"); + assert.throws( + () => verifyRawPopulation(root, expected), + /population changed/, + ); + assert.throws(() => recoveryRows(root, population, expected), /mismatch/); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/test/scripts/qualify-jsconfuser-vm/replay.test.js b/test/scripts/qualify-jsconfuser-vm/replay.test.js new file mode 100644 index 00000000..7b6157b1 --- /dev/null +++ b/test/scripts/qualify-jsconfuser-vm/replay.test.js @@ -0,0 +1,57 @@ +// Native oracle controls: run only after the separately accepted native gate. +import fs from "node:fs"; +import assert from "node:assert/strict"; +import { test } from "vitest"; +import { + inspectCurrent, + assessCurrent, +} from "../../../scripts/qualify-jsconfuser-vm/oracles.mjs"; +import { observeKnown } from "../../../scripts/qualify-jsconfuser-vm/worker.mjs"; +const root = new URL( + "../../../test/vm/jsconfuser-vm/fixtures/minified-class-boot/", + import.meta.url, +); +const source = fs.readFileSync(new URL("source.js", root), "utf8"); +const encoded = fs.readFileSync(new URL("encoded-169.js", root), "utf8"); +test("historical good output cannot conceal wrong current emitted behavior", async () => { + const historical = inspectCurrent(encoded).publicRecord; + assert.equal(observeKnown(historical.output).value, 0); + const output = `${historical.output}\nwindow.TEST_OUTPUT = "wrong-current-output";`; + const current = inspectCurrent(encoded, () => ({ + ...historical, + output, + result: { ...historical.result, output }, + })); + assert.equal(current.checks.ok, true); + assert.equal(current.checks.recoveredCoverage, true); + const result = await assessCurrent( + current, + source, + { encoded, value: 0 }, + observeKnown, + ); + assert.equal(result.qualified, false); + assert.equal(result.status, "behavior-mismatch"); + assert.equal( + result.behavior.decodedObservation.value, + "wrong-current-output", + ); +}); +test("current decline cannot inherit historical native success", async () => { + const current = inspectCurrent(encoded, (input) => ({ + ok: false, + result: null, + output: input, + diagnostic: { code: "control-decline" }, + })); + const result = await assessCurrent( + current, + source, + { encoded, value: 0 }, + () => { + throw new Error("No target behavior may execute after current decline"); + }, + ); + assert.equal(result.status, "declined"); + assert.equal(result.qualified, false); +}); diff --git a/test/scripts/qualify-jsconfuser-vm/timing.test.js b/test/scripts/qualify-jsconfuser-vm/timing.test.js new file mode 100644 index 00000000..f0b7210f --- /dev/null +++ b/test/scripts/qualify-jsconfuser-vm/timing.test.js @@ -0,0 +1,66 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { + timingSourceRoles, + observeTimingKnown, + runTimingNative, +} from '../../../scripts/qualify-jsconfuser-vm/timing-observation.mjs' +import { assertTimingBehavior } from '../../../scripts/qualify-jsconfuser-vm/timing.mjs' + +const fixture = (name) => + fs.readFileSync( + new URL( + `../../vm/jsconfuser-vm/fixtures/${name}/encoded.js`, + import.meta.url, + ), + 'utf8', + ) + +test('transport/table emitted-effects detector sees present, absent and active-timing inputs', () => { + const baseline = fixture('reference') + expect(timingSourceRoles(baseline).emitted).toEqual({ + numericTransport: false, + handlerTable: false, + }) + for (const [name, expected] of [ + ['handler-table-boot', { numericTransport: false, handlerTable: true }], + [ + 'encoded-bytecode-arithmetic', + { numericTransport: true, handlerTable: false }, + ], + ]) { + const source = fixture(name) + expect(timingSourceRoles(source).emitted).toEqual(expected) + expect( + timingSourceRoles( + source.replace( + 'var TIMING_CHECKS = false;', + 'var TIMING_CHECKS = 100;', + ), + ).emitted, + ).toEqual(expected) + } + const table = fixture('handler-table-boot') + const damaged = table.replace('this[op]();', 'this[op + 1]();') + expect(damaged).not.toBe(table) + expect(() => timingSourceRoles(damaged)).toThrow('Timing shell is not proved') +}) + +test('known-source real pause has observed stop/resume and completed child process', async () => { + const source = 'window.pauseGate(); window.TEST_OUTPUT = 7;' + const paused = await runTimingNative({ source, mode: 'paused' }) + expect(paused.result.behavior).toEqual({ kind: 'value', value: 7 }) + expect(paused.result.blockedMs).toBeGreaterThan(100) + expect(paused.process).toMatchObject({ + exitCode: 0, + signal: null, + timedOut: false, + pause: { signal: 'SIGSTOP', resumeSignal: 'SIGCONT' }, + }) + expect(() => + assertTimingBehavior( + observeTimingKnown('window.TEST_OUTPUT = 8;').behavior, + paused.result.behavior, + ), + ).toThrow() +}) diff --git a/test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/encoded.js new file mode 100644 index 00000000..0bbee4a4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/encoded.js @@ -0,0 +1,1231 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */"finished", /* 5 */undefined, /* 6 */0, /* 7 */"Error", /* 8 */"negative", /* 9 */"message", /* 10 */true]; +var BYTECODE = [67, 1, 47, 3, 69, 1, 9, 0, 0, 65, 3, 2, 62, 3, 0, 0, 61, 1, 4, 0, 61, 2, 5, 0, 71, 5, 6, 42, 5, 2, 1, 6, 61, 3, 6, 0, 42, 7, 2, 1, 6, 62, 6, 0, 0, 61, 4, 8, 0, 68, 8, 9, 6, 48, 6, 3, 5, 7, 9, 69, 4, 6, 3, 61, 5, 3, 0, 63, 3, 67, 2, 72, 5, 167, 149, 4, 54, 126, 3, 61, 6, 6, 0, 73, 7, 6, 0, 74, 105, 7, 62, 6, 0, 7, 61, 8, 7, 0, 44, 8, 6, 1, 7, 70, 8, 61, 2, 6, 0, 75, 6, 7, 0, 55, 65, 7, 5, 66, 169, 4, 55, 64, 149, 55, 64, 143, 61, 9, 6, 0, 68, 6, 7, 3, 65, 7, 5, 66, 171, 4, 55, 64, 149, 55, 66, 173, 4, 64, 149, 62, 6, 0, 0, 61, 4, 7, 0, 61, 10, 8, 0, 69, 7, 8, 6, 76, 4, 70, 5, 63, 5, 63, 5, 61, 5, 6, 0, 63, 6]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + this[op](); + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var VMPrototype = VM.prototype; +VMPrototype[OP.LOAD_CONST] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[OP.LOAD_INT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[OP.LOAD_GLOBAL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[OP.LOAD_UPVALUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); +}; +VMPrototype[OP.LOAD_THIS] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[OP.MOVE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; +}; +VMPrototype[OP.STORE_GLOBAL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; +}; +VMPrototype[OP.STORE_UPVALUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); +}; +VMPrototype[OP.GET_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.SET_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[OP.DELETE_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; +}; +VMPrototype[OP.ADD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; +}; +VMPrototype[OP.SUB] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; +}; +VMPrototype[OP.MUL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; +}; +VMPrototype[OP.DIV] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; +}; +VMPrototype[OP.MOD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; +}; +VMPrototype[OP.EXP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); +}; +VMPrototype[OP.BAND] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; +}; +VMPrototype[OP.BOR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; +}; +VMPrototype[OP.BXOR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; +}; +VMPrototype[OP.SHL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; +}; +VMPrototype[OP.SHR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; +}; +VMPrototype[OP.USHR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; +}; +VMPrototype[OP.LT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; +}; +VMPrototype[OP.GT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; +}; +VMPrototype[OP.LTE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; +}; +VMPrototype[OP.GTE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; +}; +VMPrototype[OP.EQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; +}; +VMPrototype[OP.NEQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; +}; +VMPrototype[OP.LOOSE_EQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; +}; +VMPrototype[OP.LOOSE_NEQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; +}; +VMPrototype[OP.IN] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; +}; +VMPrototype[OP.INSTANCEOF] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_NEG] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_POS] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_NOT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_BITNOT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; +}; +VMPrototype[OP.TYPEOF] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; +}; +VMPrototype[OP.VOID] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + this._operand(); // consumes argument (intended) + // consumes argument (intended) + regs[base + dst] = undefined; +}; +VMPrototype[OP.TYPEOF_SAFE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; +}; +VMPrototype[OP.JUMP] = function () { + var fp = this._f; + var regs = this._regs; + regs[fp + SLOTS.PC] = this._operand(); +}; +VMPrototype[OP.JUMP_IF_FALSE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[OP.JUMP_IF_TRUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[OP.CALL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } +}; +VMPrototype[OP.CALL_METHOD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[OP.NEW] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } +}; +VMPrototype[OP.RETURN] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[OP.THROW] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + throw regs[base + this._operand()]; +}; +VMPrototype[OP.MAKE_CLOSURE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + // unified prototype for new/instanceof + regs[base + dst] = shell; +}; +VMPrototype[OP.BUILD_ARRAY] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; +}; +VMPrototype[OP.BUILD_OBJECT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; +}; +VMPrototype[OP.DEFINE_GETTER] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); +}; +VMPrototype[OP.DEFINE_SETTER] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); +}; +VMPrototype[OP.FOR_IN_SETUP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; +}; +VMPrototype[OP.FOR_IN_NEXT] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } +}; +VMPrototype[OP.TRY_SETUP] = function () { + var fp = this._f; + var regs = this._regs; + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); +}; +VMPrototype[OP.TRY_END] = function () { + var fp = this._f; + var regs = this._regs; + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); +}; +VMPrototype[OP.FINALLY_SETUP] = function () { + var fp = this._f; + var regs = this._regs; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); +}; +VMPrototype[OP.PATCH] = function () { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } +}; +VMPrototype[OP.JUMP_REG] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; +}; +VMPrototype[OP.DEBUGGER] = function () { + debugger; +}; +VMPrototype[61] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_LOAD_CONST_1_0_2 (order: [1,0,2]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0], _unsortedOperands[2]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); +}; +VMPrototype[62] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_LOAD_GLOBAL_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[63] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[64] = function () { + var fp = this._f; + var regs = this._regs; + // ALIAS_JUMP_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + regs[fp + SLOTS.PC] = _operands[0]; +}; +VMPrototype[65] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_MOVE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; +}; +VMPrototype[66] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_LOAD_INT_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = _operands[1]; +}; +VMPrototype[67] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // ALIAS_LOAD_THIS_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[68] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_GET_PROP_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + // dst = regs[obj][regs[key]] + var dst = _operands[0]; + var obj = regs[base + _operands[1]]; + var key = regs[base + _operands[2]]; + regs[base + dst] = obj[key]; +}; +VMPrototype[69] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_SET_PROP_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + _operands[0]]; + var key = regs[base + _operands[1]]; + var val = regs[base + _operands[2]]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[70] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_THROW_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + throw regs[base + _operands[0]]; +}; +VMPrototype[71] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_UNARY_NEG_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = -regs[base + _operands[1]]; +}; +VMPrototype[72] = function () { + var fp = this._f; + var regs = this._regs; + // ALIAS_FINALLY_SETUP_2_3_0_1 (order: [2,3,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[1]]; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: _operands[0], + contReg: _operands[1], + payloadReg: _operands[2], + throwPad: _operands[3] + }); +}; +VMPrototype[73] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_LT_0_2_1 (order: [0,2,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a < regs[base + _operands[2]]; +}; +VMPrototype[74] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // ALIAS_JUMP_IF_FALSE_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var src = _operands[0]; + var target = _operands[1]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[75] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_ADD_2_0_1 (order: [2,0,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[0]]; + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a + regs[base + _operands[2]]; +}; +VMPrototype[76] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // ALIAS_JUMP_REG_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + _operands[0]]; +}; +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/qualification.json new file mode 100644 index 00000000..118df14d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/qualification.json @@ -0,0 +1,73 @@ +{ + "id": "exceptions--aliasedOpcodesHandlerTable--124", + "case": "exceptions", + "profile": "aliasedOpcodesHandlerTable", + "seed": 124, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": true, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": true, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "emitted-representation-components", + "observed": true, + "components": { + "aliasedOpcodes": true, + "handlerTable": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/alias-handler-table-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/encoded.js b/test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/encoded.js new file mode 100644 index 00000000..3949103a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"O1Lezd1GyNq2Ty06", /* 1 */"eUFzy2C/WCtF51Z5WZFXGVaDWQ9Nkw==", /* 2 */-1825795505, /* 3 */-1100832952, /* 4 */1174738870, /* 5 */453096806, /* 6 */undefined]; +var BYTECODE = [4, 1, 2, 2, 0, 2568121930, 0, 3, 1, 3561773946, 0, 4, 2, 2469171785, 0, 5, 3, 3194134351, 13, 6, 4, 5, 0, 4, 4, 1174738869, 11, 5, 6, 4, 0, 4, 5, 453096803, 15, 6, 5, 4, 9, 2, 3, 6, 0, 2, 6, 0, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 7; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/qualification.json b/test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/qualification.json new file mode 100644 index 00000000..0b0fbe00 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "arithmetic--concealConstants--25", + "case": "arithmetic", + "profile": "concealConstants", + "seed": 25, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": { + "kind": "value", + "value": 0 + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/source.js b/test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-arithmetic/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/encoded.js b/test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/encoded.js new file mode 100644 index 00000000..85951914 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */-1726845367, /* 1 */"WkFfy12/aCt15255", /* 2 */"doci+bCRKhm2oBsvitMGR2TBbFkeMQ==", /* 3 */undefined, /* 4 */-1100832946]; +var BYTECODE = [4, 1, 47, 4, 55, 1, 5, 0, 0, 5, 2, 4, 0, 4, 0, 2568121930, 42, 5, 2, 1, 4, 5, 3, 5, 2, 4, 1, 3561773946, 0, 5, 2, 2469171785, 42, 6, 3, 0, 42, 7, 3, 0, 48, 8, 2, 6, 7, 9, 4, 5, 8, 0, 4, 3, 0, 45, 4, 4, 2, 5, 3, 0, 47, 4, 77, 0, 3, 1, 0, 1, 3, 45, 4, 0, 4, 3, 0, 45, 4, 3, 0, 0, 0, 1, 4, 3194134351, 11, 2, 0, 1, 7, 0, 2, 45, 2]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 9; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/qualification.json b/test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/qualification.json new file mode 100644 index 00000000..54c873d5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/qualification.json @@ -0,0 +1,66 @@ +{ + "id": "closures--concealConstants--25", + "case": "closures", + "profile": "concealConstants", + "seed": 25, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/source.js b/test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/source.js new file mode 100644 index 00000000..8148a2c5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-closures/source.js @@ -0,0 +1 @@ +function counter(start) { let value = start; return () => ++value; } const next = counter(3); window.TEST_OUTPUT = [next(), next()]; diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/encoded.js b/test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/encoded.js new file mode 100644 index 00000000..ddd227da --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */-1726845365, /* 1 */-733193352, /* 2 */-1825795510, /* 3 */-1100832949, /* 4 */1174738864, /* 5 */453096805, /* 6 */"z6TSPSCstVQ7xJlP", /* 7 */"E5y5DBJ0kuUbrIA8b9RhTID8B1SD1Q==", /* 8 */undefined]; +var BYTECODE = [4, 1, 0, 5, 0, 2568121930, 0, 6, 1, 3561773946, 48, 7, 2, 5, 6, 5, 2, 7, 0, 5, 2, 2469171785, 0, 6, 3, 3194134351, 48, 7, 2, 5, 6, 5, 3, 7, 0, 5, 4, 1174738869, 0, 6, 5, 453096803, 48, 7, 2, 5, 6, 5, 4, 7, 2, 5, 6, 1836679046, 0, 6, 7, 1941740078, 48, 7, 3, 2, 3, 4, 9, 5, 6, 7, 0, 5, 8, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 8; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/qualification.json b/test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/qualification.json new file mode 100644 index 00000000..34ebb287 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/qualification.json @@ -0,0 +1,96 @@ +{ + "id": "repeatedArrays--concealConstants--25", + "case": "repeatedArrays", + "profile": "concealConstants", + "seed": 25, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ], + [ + 5, + 6 + ] + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ], + [ + 5, + 6 + ] + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + [ + 1, + 2 + ], + [ + 3, + 4 + ], + [ + 5, + 6 + ] + ] + }, + "effectObservation": { + "axis": "serialized-bytecode", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/source.js b/test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/source.js new file mode 100644 index 00000000..d9e21348 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/concealed-constants-repeatedArrays/source.js @@ -0,0 +1 @@ +const a = [1, 2]; const b = [3, 4]; const c = [5, 6]; window.TEST_OUTPUT = [a, b, c]; diff --git a/test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/encoded.js new file mode 100644 index 00000000..50aceed9 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/encoded.js @@ -0,0 +1,2420 @@ +var CONSTANTS = [/* 0 */-1378679428, /* 1 */"O9WMLhS72SfKat3+zoU=", /* 2 */640384011, /* 3 */"IE2Q3S1FXjN132tjjeQLHbCNGAXqhAEP/3sL5/Ks+j2b1d5EH//qUyXXs7xQLaWV9HTLjOkb5LfZLOOczeXCdcfcx0zNR+vc6GzE5ddlwyzJvP4H/IvcfAWUjSUIvJwsP1TMw+dM39SPNT2s/BwXdKkLcJy1BHa0hB0O7FRkWNQ3TO5EFsS7TSX8o2QhpEY8UZRnBGZ8VAwqlGQkSqxJVGv0ZEx+3G60XDRe3E5kNPQ0HIqNEwSRlBLsnHsZ5E2sATx7xJBECfyJUw8kqrwhLJyVOASPjIYb87f5LC5coPUBdI7cDUzBt83c/WzE5cGV9QzMvPgH+4vAfK3luSX1vNrMykfXw9pMnNTdJSCtpxwPd5ELM5+vNPu10h3V7AV3kdMoT59ECNWobQz9jWQmq0M/ZYt6BVt9Oe2JpA8j2a8LU7fEcE1C3WC1cCt6n2hrTvdmHUSNTjVmk0HvZGMC135NRz1FxStVCf+qazEnv7o0Db6FFwV+j0MTdaevIgxdufUiRZbeAkuZt3nb0m7o9d+Vpg5pg7YXEZPWfl/l1lXCvsbKxkfc++5u0NrwJeitlhLjh+sD4Z6jMuOlxF3u6tx2y9s5Tqs6VNa8bRPihxaGk9Y/94IIFpZ9Meqiphk6nM90WpnGBEpy0nGmfyK8nwFy2oY0ApCNDjZImljuWXpF1kdKeb5y1jxSMf5HYnwWa7JbDjuGBnp07kwaN6aeKgler8kIQpLeJTKXpxoiZm6h8W6a6A4Iuo0XEeqjfmzqo1F7vsnC0VfF879uHaLWOUmO1Boohq8L7p7uKtCh4Fmv8u1G58PDT8cyxdnLYab63hbVi/o/wIrXFoV5V+HVVlwzjM8yQ/f2n3Ho2fimKyucnz0LqIYNGf6xCTm5olnva3NRx01BuLkc0Y5qC//Za0IXbrkYAWKZZXFe7ygjVbcxMzRZN8F6SUXfdit+p3IbZnFt+XuBlwgNs4knJ+OteCHRdklYwWbL+lc2+4JgDJnLMS+JmQA2d6vzgpjeIP6pGVH0yFpHp9svr5o406HkaYPw5wjsg+w/0ZDi6MVhM+iyULY74M/jWInwmGnLIeqoUTCgnxkAzIgJEL+pDSD6WPvP7XuKwAdIgbFvKKtgOvjvkyMIpIABCAGQS3g34JRbVrCIyBNA2MlZcFLYUyB8qF4QXYh2AGSYTRBUuEIgeuhxcFnICkBHOAHQLFgX4LJoKJDPOViA7xhjcELod6AXKJ+xMliKwHZ4tdMbqLAgMZj1EfT4wwDyi3EwgqhX4K95OdCNWLbD4c+AUJz4/WHDGeaw2gjon9YIwfCuGc6h3DjJUO3P/0P/2CVRqrk3IdhoaP+hi8UAwLiHEQiZ42AP75FTEreWwAVZtPEbaXzfDSt+t14bCoGL+SqRkShWo0AnbdNceWLBTEl7MUqrXGdr+7luSZlvAVeJJxNxd3jjfF9MMWHJXEHSylx3xdt0r+06d5F3CWGBIQepkxw/sqI16Y1xKcmeRmK7xc/KWvTmkmlVEYG2IQPdnypz7vahYcOpxDbtWrvPx3vo1vEqkaHuJtSS1//Xg8cGhrLcKfDhkHpT7oVbEsaPyhFeh+nUYsaenDNxto0COo6k8QWapC4Juo5XJsp1flPKfVKKPl+isGdK8okOH4JNOnCOXhoPZn7qYF5VykvGVl5F8j52c+J+rlYyUZYcDhv6Tha/qxY+u6og1oZ9F8KnRlmycy6aYgDmtm0eGrGGcwrwnztqg+bGXYat1jb9QqfPUHK11j8i3b2hFu2K6f/ai98W57qoLdflzHLqzuBDo8b8sqddh+X6au+ekQv/h9QaLI2E9Vplvy5ss8ZX1ML4fovVHVWTPgWrxBcM+/gOOYWlNXutWONEZ3Lzll5KxZvFHF2j6lBnYht8r0s1RYVwDXP9SxdHoxE/M1Vd1Sp9X81vV1S7Si9qmx71ho0HHZaEAzOUny5jc+VzXWDNh8RtW4/PE3uF5Awtmr09FeKE5v+Ck54UDb2qLS7Vp3Rez7hb6jTirPJd8OXsdPOc8wP1h+ocT2z55cNk5SzOu9hHwcyDfPPVFqSQPAqs1Rci/LcMNJTUNdKsPNyBdzbM+VwpxLU1B1yu7CFkQBxYjHMERIQxDKv8JmSFq1w8ulRQxFJ8BMxXVGekQSxllDj0bIxgHE+0WDRlnHdkV/RXXFzMK8QsVLDvPWSs5A0smb0CBHiEf/wtlLMkFLypLTvUi0QYzOpbGDT0rNMd2PXZdGicqANJtL0cCGzO5dVUwcz4u8NH48zjfOjlzqWYPMWTuge3/OYM05X2JZus09zQdzbL3lwvtbs1KF247Px3/xPfjA8F7ZUTbYL8JGfzqzg/7kQcxVV9e+1LJ3+jMy+KlAYF2I0=", /* 4 */"ofIwdsIdw4rNNtub", /* 5 */530117895, /* 6 */659186317, /* 7 */undefined, /* 8 */"t5QIJLOtIFyD9GZE5t8=", /* 9 */"q1kQ4a14K5dYCw==", /* 10 */1988821584, /* 11 */1751649804, /* 12 */-750471943, /* 13 */-83807761, /* 14 */-364194793, /* 15 */-2136851672, /* 16 */-2139124995, /* 17 */-1065561677, /* 18 */-1615144670, /* 19 */1274803317, /* 20 */1846895339, /* 21 */-560823555, /* 22 */1847871366, /* 23 */348318556, /* 24 */-593019949, /* 25 */258017264, /* 26 */-1452670994, /* 27 */-88010861, /* 28 */-647169027, /* 29 */-2019760952, /* 30 */-1524123292, /* 31 */-403177877, /* 32 */-1166804882, /* 33 */340359194, /* 34 */-487731745, /* 35 */-2130579283, /* 36 */1882909879, /* 37 */566204806, /* 38 */"LV6Y9e1Kz97XSw==", /* 39 */true, /* 40 */531373747, /* 41 */1271560744, /* 42 */"2W/i8MBYnME=", /* 43 */-2011299966, /* 44 */503338632, /* 45 */130666786, /* 46 */-827238255, /* 47 */1034178503, /* 48 */197620266, /* 49 */-256529936, /* 50 */253652093, /* 51 */-287340696, /* 52 */-84714795, /* 53 */-1768645081, /* 54 */-1921291789, /* 55 */2107009171, /* 56 */-1833643850, /* 57 */1515340732, /* 58 */382461831, /* 59 */-989397461, /* 60 */"Vj2mrT7Vt3o=", /* 61 */"", /* 62 */"rd4Bc5r3NmNNrUc1", /* 63 */"6X3w5euk5Dz5k98H8fzsDNaVByWbrAfb", /* 64 */1738749827, /* 65 */"Zv9xE32niSIBrZDV", /* 66 */"p5wmNaK1Nh2KjBV3kJM2L5+kIdU=", /* 67 */"nHdfEomOFgKLlQ==", /* 68 */"IouMFGS9kC045Y10", /* 69 */"FY/uAuK1yy3Wmt3m", /* 70 */-1191481806, /* 71 */1580608808, /* 72 */-1597669921, /* 73 */1085253274, /* 74 */-1004169800, /* 75 */-1884913818, /* 76 */-615789670, /* 77 */730612268, /* 78 */1391217945, /* 79 */1297685459, /* 80 */-1843330387, /* 81 */-842531367, /* 82 */-1944209724, /* 83 */-2017917079, /* 84 */-792209049, /* 85 */-1671005727, /* 86 */-968112533, /* 87 */-217343408, /* 88 */-1182426169, /* 89 */-111349041, /* 90 */1627377097, /* 91 */-285776313, /* 92 */-1206415132, /* 93 */1270104338, /* 94 */249256050, /* 95 */732955578, /* 96 */169878433, /* 97 */"YXPn3mZF", /* 98 */"kSAJuZBRB8ncQPbL7Fc=", /* 99 */623020822, /* 100 */2123538476, /* 101 */1819052275, /* 102 */"v/ccYr0uIrI=", /* 103 */"Q7hEIEhoTvA=", /* 104 */-1913858688, /* 105 */1704232671, /* 106 */"gNMBRqnCJs77USH6", /* 107 */1290322204, /* 108 */2040666193, /* 109 */-748278794, /* 110 */1334127986, /* 111 */1877338952, /* 112 */134278264, /* 113 */-1385414686, /* 114 */-1182077446, /* 115 */"q9IyfrvFQEqgPg2qqmdl+g==", /* 116 */"8T7tqrph8PnSEtqX0QPejg==", /* 117 */-649233566, /* 118 */585589482, /* 119 */468083219]; +var BYTECODE = [82, 83, 84, 47, 6, 3558, 1, 76, 0, 0, 85, 42, 5, 6, 1, 7, 47, 8, 5949, 4, 98, 0, 0, 86, 47, 15, 117, 1, 107, 1, 0, 1, 5, 61, 15, 12, 14, 14, 12, 14, 14, 13, 2, 12, 87, 88, 89, 90, 42, 3, 8, 4, 5, 9, 10, 11, 141, 5, 530117894, 16, 62, 12, 12, 17, 13, 12, 12, 12, 14, 12, 13, 13, 16, 13, 42, 16, 2, 1, 17, 141, 6, 659186313, 17, 42, 18, 2, 1, 17, 91, 120, 9, 778833906, 10, 146, 11, 8, 42, 4, 8, 4, 5, 9, 10, 11, 92, 48, 17, 3, 16, 18, 19, 93, 141, 7, 0, 15, 94, 124, 103, 8, 775716291, 102, 9, 4273292510, 104, 10, 1988861575, 101, 11, 1751664521, 141, 12, 3544495353, 105, 47, 97, 9456, 2, 12, 0, 0, 95, 141, 13, 4211175398, 20, 61, 20, 103, 103, 103, 103, 103, 103, 103, 21, 101, 141, 12, 3544495353, 22, 61, 22, 101, 102, 102, 103, 102, 102, 101, 23, 101, 141, 14, 3930726354, 24, 63, 101, 24, 102, 21, 101, 101, 102, 101, 103, 101, 25, 102, 102, 101, 101, 64, 101, 99, 102, 103, 103, 102, 103, 25, 65, 1, 104, 102, 104, 99, 102, 211, 99, 211, 62, 101, 102, 99, 101, 101, 102, 102, 101, 101, 103, 103, 99, 103, 115, 95, 3190140315, 100, 3935425921, 66, 95, 101, 100, 101, 101, 102, 103, 101, 0, 103, 101, 101, 102, 100, 103, 67, 103, 100, 103, 100, 99, 103, 101, 101, 101, 68, 6710, 102, 103, 102, 100, 95, 95, 140, 2350489799, 96, 77, 141, 15, 2158126566, 26, 61, 26, 101, 101, 103, 102, 102, 101, 102, 23, 102, 69, 21, 103, 27, 101, 103, 102, 101, 102, 101, 102, 101, 23, 64, 101, 99, 102, 101, 101, 102, 101, 27, 65, 0, 104, 101, 104, 99, 102, 315, 99, 315, 115, 95, 675290617, 100, 235873619, 66, 95, 102, 100, 103, 101, 102, 102, 101, 1, 103, 103, 101, 103, 100, 103, 70, 100, 101, 103, 100, 101, 103, 103, 99, 102, 103, 68, 23303, 102, 103, 102, 100, 95, 95, 140, 1218297768, 96, 77, 115, 95, 3064852834, 96, 1169280755, 77, 141, 16, 2155860442, 28, 66, 28, 102, 29, 103, 103, 101, 103, 101, 2, 102, 103, 103, 102, 23, 102, 61, 29, 102, 101, 102, 103, 102, 101, 103, 23, 102, 69, 21, 101, 30, 101, 102, 102, 102, 101, 101, 101, 101, 23, 64, 101, 99, 102, 102, 101, 101, 102, 30, 65, 2, 104, 101, 104, 99, 101, 428, 99, 428, 115, 95, 882690955, 100, 440931069, 66, 95, 103, 100, 101, 102, 101, 102, 101, 0, 103, 101, 103, 103, 100, 103, 70, 99, 102, 102, 100, 103, 102, 102, 100, 102, 101, 68, 1502, 101, 103, 102, 100, 95, 95, 140, 2062759971, 96, 77, 115, 95, 162908835, 96, 2899969086, 77, 141, 17, 3229382071, 31, 68, 30994, 102, 102, 101, 31, 32, 23, 61, 32, 101, 103, 102, 102, 101, 101, 101, 23, 103, 69, 21, 101, 33, 103, 102, 102, 101, 101, 102, 103, 101, 23, 64, 102, 99, 103, 101, 101, 101, 102, 33, 65, 2, 104, 103, 104, 99, 103, 533, 99, 533, 62, 102, 101, 99, 102, 101, 102, 102, 102, 102, 102, 102, 99, 102, 115, 95, 2985012365, 100, 2547447783, 66, 95, 102, 100, 101, 103, 103, 102, 101, 1, 101, 101, 101, 101, 100, 101, 67, 102, 99, 102, 100, 100, 101, 101, 103, 102, 68, 34866, 103, 103, 101, 100, 95, 95, 140, 2889968483, 96, 77, 115, 95, 3512126421, 96, 3115283494, 77, 141, 18, 2679804032, 34, 66, 34, 101, 35, 101, 101, 103, 102, 101, 0, 102, 102, 101, 101, 23, 101, 61, 35, 102, 103, 102, 102, 102, 103, 102, 23, 102, 69, 21, 103, 36, 103, 101, 101, 102, 102, 102, 101, 103, 23, 64, 103, 99, 103, 102, 103, 103, 101, 36, 65, 0, 104, 101, 104, 99, 103, 659, 99, 659, 115, 95, 4121893900, 100, 3684312934, 66, 95, 102, 100, 101, 101, 102, 103, 102, 2, 102, 103, 102, 102, 100, 102, 70, 100, 101, 101, 100, 102, 101, 103, 99, 101, 102, 68, 12355, 102, 102, 102, 100, 95, 95, 140, 2534237707, 96, 77, 115, 95, 3929470628, 96, 3577128565, 77, 141, 19, 1274801132, 37, 66, 37, 102, 38, 102, 103, 102, 102, 103, 1, 102, 103, 102, 101, 23, 102, 61, 38, 103, 101, 102, 102, 101, 102, 103, 23, 101, 69, 21, 101, 39, 101, 103, 102, 102, 101, 103, 102, 101, 23, 64, 103, 99, 103, 101, 102, 102, 101, 39, 65, 2, 104, 101, 104, 99, 101, 772, 99, 772, 115, 95, 1054324377, 100, 478343667, 66, 95, 102, 100, 102, 102, 103, 102, 101, 1, 102, 101, 101, 103, 100, 102, 70, 99, 101, 101, 100, 102, 101, 101, 100, 102, 103, 68, 2811, 101, 101, 102, 100, 95, 95, 140, 2190916041, 96, 77, 115, 95, 1531214907, 96, 3207782003, 77, 141, 20, 1846898425, 40, 66, 40, 101, 41, 103, 101, 103, 102, 103, 1, 101, 101, 103, 102, 23, 103, 61, 41, 101, 101, 101, 102, 101, 101, 101, 23, 101, 69, 21, 101, 42, 101, 101, 102, 103, 102, 103, 102, 102, 23, 64, 101, 99, 101, 102, 103, 103, 103, 42, 65, 2, 104, 101, 104, 99, 103, 885, 99, 885, 62, 101, 101, 99, 102, 101, 102, 103, 101, 102, 103, 101, 99, 103, 115, 95, 283349604, 100, 4138655198, 66, 95, 103, 100, 101, 103, 103, 103, 101, 0, 103, 102, 101, 102, 100, 102, 67, 101, 100, 101, 100, 99, 102, 101, 102, 102, 68, 22696, 103, 102, 101, 100, 95, 95, 140, 3392597994, 96, 77, 115, 95, 2402837824, 96, 295888324, 77, 141, 21, 3734147314, 43, 66, 43, 101, 44, 102, 101, 103, 102, 101, 1, 102, 101, 101, 103, 23, 102, 61, 44, 103, 102, 101, 103, 102, 103, 103, 23, 102, 69, 21, 102, 45, 103, 103, 102, 103, 103, 103, 103, 103, 23, 64, 103, 99, 102, 102, 103, 102, 101, 45, 65, 1, 104, 103, 104, 99, 101, 1011, 99, 1011, 115, 95, 2917409927, 100, 2480107513, 66, 95, 102, 100, 101, 103, 101, 102, 103, 1, 101, 103, 102, 102, 100, 102, 70, 99, 101, 101, 100, 101, 102, 102, 100, 101, 102, 68, 39225, 103, 102, 101, 100, 95, 95, 140, 1304710134, 96, 77, 115, 95, 4210290596, 96, 3242730197, 77, 141, 22, 1847876047, 46, 66, 46, 103, 47, 101, 101, 103, 101, 102, 0, 103, 101, 101, 101, 23, 103, 61, 47, 103, 102, 101, 102, 102, 103, 103, 23, 101, 69, 21, 103, 48, 101, 102, 103, 101, 102, 103, 102, 102, 23, 64, 101, 99, 102, 101, 101, 102, 102, 48, 65, 2, 104, 101, 104, 99, 101, 1124, 99, 1124, 115, 95, 1333596959, 100, 757619313, 66, 95, 102, 100, 102, 101, 101, 103, 101, 2, 103, 102, 102, 102, 100, 101, 70, 100, 101, 103, 100, 101, 101, 102, 99, 103, 103, 68, 1439, 102, 103, 103, 100, 95, 95, 140, 3537748903, 96, 77, 115, 95, 3556464178, 96, 3031376952, 77, 141, 23, 348313342, 49, 68, 55670, 102, 102, 102, 49, 50, 23, 61, 50, 103, 103, 101, 101, 103, 103, 103, 23, 103, 69, 21, 102, 51, 103, 102, 103, 102, 102, 102, 102, 102, 23, 64, 102, 99, 103, 103, 101, 102, 103, 51, 65, 2, 104, 103, 104, 99, 103, 1229, 99, 1229, 115, 95, 798920389, 100, 223204927, 66, 95, 103, 100, 103, 103, 102, 102, 103, 2, 101, 102, 101, 103, 100, 103, 70, 100, 102, 103, 100, 103, 103, 101, 99, 102, 101, 68, 32886, 103, 103, 103, 100, 95, 95, 140, 2621108286, 96, 77, 115, 95, 3347425060, 96, 2236735367, 77, 141, 24, 3701934575, 52, 66, 52, 103, 53, 102, 102, 101, 102, 102, 1, 103, 102, 102, 103, 23, 101, 61, 53, 102, 103, 103, 103, 102, 102, 103, 23, 102, 69, 21, 103, 54, 101, 103, 101, 102, 103, 103, 103, 103, 23, 64, 103, 99, 103, 103, 102, 103, 103, 54, 65, 0, 104, 103, 104, 99, 101, 1342, 99, 1342, 115, 95, 2538847526, 100, 1964950680, 66, 95, 101, 100, 103, 103, 103, 103, 102, 2, 101, 103, 102, 102, 100, 102, 70, 99, 102, 101, 100, 101, 101, 102, 100, 101, 101, 68, 56216, 103, 103, 102, 100, 95, 95, 140, 1796777992, 96, 77, 115, 95, 907435384, 96, 2279450133, 77, 141, 25, 258016857, 55, 66, 55, 101, 56, 102, 103, 103, 101, 101, 0, 101, 102, 101, 102, 23, 101, 61, 56, 101, 101, 101, 102, 103, 101, 103, 23, 101, 69, 21, 103, 57, 101, 102, 101, 101, 101, 101, 102, 101, 23, 64, 102, 99, 101, 102, 103, 102, 103, 57, 65, 2, 104, 101, 104, 99, 103, 1455, 99, 1455, 62, 102, 102, 99, 102, 102, 103, 101, 102, 101, 102, 103, 99, 103, 115, 95, 2559672370, 100, 2120010596, 66, 95, 103, 100, 101, 103, 101, 102, 102, 0, 103, 102, 103, 102, 100, 103, 67, 103, 99, 101, 100, 100, 102, 103, 103, 101, 68, 51458, 103, 103, 101, 100, 95, 95, 140, 2712074026, 96, 77, 115, 95, 2597574226, 96, 3553001214, 77, 141, 26, 2842256731, 58, 68, 54339, 101, 101, 103, 58, 59, 23, 61, 59, 102, 101, 103, 103, 103, 102, 103, 23, 103, 69, 21, 102, 60, 102, 101, 103, 103, 101, 101, 101, 102, 23, 64, 101, 99, 102, 101, 103, 103, 102, 60, 65, 1, 104, 101, 104, 99, 101, 1573, 99, 1573, 115, 95, 789488131, 100, 318631285, 66, 95, 102, 100, 103, 102, 103, 103, 103, 0, 101, 102, 101, 102, 100, 101, 70, 100, 101, 101, 100, 101, 101, 103, 99, 103, 102, 68, 29282, 101, 102, 102, 100, 95, 95, 140, 2659695004, 96, 77, 115, 95, 20007044, 96, 2307415832, 77, 141, 27, 4206982923, 61, 66, 61, 103, 62, 101, 103, 103, 101, 102, 0, 103, 102, 102, 101, 23, 103, 61, 62, 102, 103, 103, 102, 102, 103, 103, 23, 102, 69, 21, 101, 63, 102, 101, 102, 103, 102, 101, 103, 101, 23, 64, 101, 99, 102, 101, 102, 103, 102, 63, 65, 1, 104, 102, 104, 99, 101, 1686, 99, 1686, 62, 102, 102, 99, 102, 103, 101, 102, 101, 102, 102, 101, 99, 101, 115, 95, 2189978600, 100, 1618457410, 66, 95, 103, 100, 103, 102, 101, 103, 101, 2, 103, 101, 101, 101, 100, 101, 67, 103, 99, 101, 100, 100, 101, 101, 101, 103, 68, 50903, 103, 101, 101, 100, 95, 95, 140, 1658440127, 96, 77, 115, 95, 1929799979, 96, 344060715, 77, 141, 28, 3647748859, 64, 68, 34657, 102, 102, 102, 64, 65, 23, 61, 65, 102, 102, 103, 103, 103, 102, 102, 23, 103, 69, 21, 101, 66, 103, 103, 101, 103, 103, 103, 101, 103, 23, 64, 103, 99, 101, 101, 102, 102, 101, 66, 65, 2, 104, 102, 104, 99, 102, 1804, 99, 1804, 62, 103, 102, 99, 101, 103, 101, 103, 103, 101, 103, 103, 99, 101, 115, 95, 4008225075, 100, 3436703845, 66, 95, 101, 100, 101, 103, 101, 103, 102, 1, 101, 102, 102, 103, 100, 101, 67, 103, 100, 101, 100, 99, 101, 102, 102, 103, 68, 44513, 102, 101, 102, 100, 95, 95, 140, 3275053280, 96, 77, 115, 95, 66832333, 96, 2122592718, 77, 141, 29, 2275192012, 67, 66, 67, 101, 68, 101, 103, 103, 101, 102, 0, 101, 101, 102, 102, 23, 103, 61, 68, 103, 102, 101, 103, 101, 102, 101, 23, 103, 69, 21, 102, 69, 102, 102, 103, 101, 102, 102, 103, 102, 23, 64, 103, 99, 103, 103, 101, 102, 101, 69, 65, 2, 104, 103, 104, 99, 101, 1930, 99, 1930, 62, 102, 103, 99, 101, 103, 101, 103, 102, 103, 103, 102, 99, 103, 115, 95, 2893809503, 100, 2452311729, 66, 95, 103, 100, 103, 101, 101, 101, 103, 0, 101, 103, 103, 101, 100, 103, 67, 103, 100, 101, 100, 99, 103, 103, 103, 102, 68, 47012, 102, 102, 102, 100, 95, 95, 140, 931995842, 96, 77, 115, 95, 550787191, 96, 2963747358, 77, 141, 30, 2770859179, 70, 66, 70, 102, 71, 102, 102, 102, 102, 102, 0, 102, 102, 102, 103, 23, 102, 61, 71, 102, 102, 103, 102, 101, 101, 102, 23, 102, 69, 21, 102, 72, 102, 101, 101, 102, 102, 102, 101, 103, 23, 64, 101, 99, 103, 101, 102, 101, 103, 72, 65, 0, 104, 103, 104, 99, 102, 2056, 99, 2056, 62, 103, 101, 99, 103, 102, 103, 103, 102, 102, 103, 102, 99, 103, 115, 95, 2313707734, 100, 1876404232, 66, 95, 102, 100, 101, 103, 101, 103, 101, 0, 103, 103, 101, 103, 100, 101, 67, 102, 99, 101, 100, 100, 101, 101, 102, 102, 68, 56264, 103, 101, 103, 100, 95, 95, 140, 2244859223, 96, 77, 115, 95, 818112735, 96, 519655857, 77, 141, 31, 3891776144, 73, 68, 48979, 103, 101, 101, 73, 74, 23, 61, 74, 101, 102, 101, 101, 102, 103, 103, 23, 101, 69, 21, 103, 75, 101, 103, 101, 101, 101, 101, 103, 103, 23, 64, 103, 99, 103, 101, 103, 103, 103, 75, 65, 2, 104, 103, 104, 99, 101, 2174, 99, 2174, 62, 102, 101, 99, 103, 101, 101, 103, 101, 101, 101, 101, 99, 101, 115, 95, 3368662408, 100, 2931096802, 66, 95, 103, 100, 102, 102, 102, 101, 103, 2, 102, 103, 102, 102, 100, 103, 67, 102, 100, 101, 100, 99, 102, 102, 102, 101, 68, 57100, 102, 101, 102, 100, 95, 95, 140, 4188690631, 96, 77, 115, 95, 89295340, 96, 2397444257, 77, 141, 32, 3128145869, 76, 66, 76, 102, 77, 101, 103, 101, 101, 101, 2, 102, 102, 103, 101, 23, 103, 61, 77, 102, 102, 103, 101, 101, 103, 103, 23, 103, 69, 21, 101, 78, 102, 102, 102, 102, 101, 101, 101, 101, 23, 64, 101, 99, 101, 102, 102, 102, 103, 78, 65, 2, 104, 103, 104, 99, 103, 2300, 99, 2300, 62, 102, 102, 99, 101, 103, 103, 101, 101, 101, 101, 102, 99, 103, 115, 95, 3754032425, 100, 3180135555, 66, 95, 101, 100, 102, 102, 103, 103, 103, 2, 103, 102, 102, 102, 100, 102, 67, 101, 99, 103, 100, 100, 103, 102, 102, 101, 68, 54286, 102, 103, 101, 100, 95, 95, 140, 1048505142, 96, 77, 115, 95, 3986333924, 96, 3032587537, 77, 141, 33, 340344986, 79, 66, 79, 103, 80, 103, 101, 103, 101, 102, 1, 103, 102, 102, 101, 23, 101, 61, 80, 101, 102, 103, 101, 102, 101, 102, 23, 103, 69, 21, 102, 81, 103, 101, 103, 101, 102, 102, 103, 103, 23, 64, 103, 99, 103, 101, 103, 101, 102, 81, 65, 0, 104, 102, 104, 99, 103, 2426, 99, 2426, 115, 95, 532943275, 100, 4256389405, 66, 95, 102, 100, 103, 102, 101, 102, 103, 2, 103, 103, 101, 102, 100, 103, 70, 99, 103, 103, 100, 102, 103, 101, 100, 102, 102, 68, 2658, 103, 102, 101, 100, 95, 95, 140, 621394145, 96, 77, 115, 95, 3698163151, 96, 1813840345, 77, 141, 34, 3807231459, 82, 68, 63470, 102, 101, 102, 82, 83, 23, 61, 83, 101, 101, 101, 101, 101, 102, 101, 23, 101, 69, 21, 103, 84, 103, 102, 101, 101, 101, 101, 101, 103, 23, 64, 103, 99, 101, 101, 101, 103, 102, 84, 65, 0, 104, 101, 104, 99, 101, 2531, 99, 2531, 115, 95, 937937775, 100, 366154433, 66, 95, 101, 100, 101, 103, 102, 102, 103, 1, 103, 101, 102, 101, 100, 101, 70, 99, 102, 102, 100, 101, 102, 101, 100, 103, 101, 68, 32422, 101, 103, 101, 100, 95, 95, 140, 2791908222, 96, 77, 115, 95, 2263913635, 96, 1401671624, 77, 141, 35, 2164364046, 85, 66, 85, 103, 86, 101, 103, 103, 101, 103, 2, 103, 103, 103, 103, 23, 103, 61, 86, 102, 103, 103, 101, 103, 101, 103, 23, 102, 69, 21, 102, 87, 103, 102, 103, 102, 101, 102, 103, 101, 23, 64, 102, 99, 101, 101, 102, 101, 101, 87, 65, 1, 104, 101, 104, 99, 101, 2644, 99, 2644, 115, 95, 3334150246, 100, 2758157272, 66, 95, 102, 100, 103, 103, 103, 101, 102, 1, 101, 101, 101, 102, 100, 102, 70, 99, 101, 101, 100, 101, 103, 102, 100, 101, 103, 68, 9983, 103, 102, 102, 100, 95, 95, 140, 474283182, 96, 77, 115, 95, 783351012, 96, 332202513, 77, 141, 36, 1882868799, 88, 68, 29046, 102, 102, 102, 88, 89, 23, 61, 89, 102, 102, 103, 102, 103, 102, 101, 23, 102, 69, 21, 103, 90, 102, 102, 103, 103, 102, 103, 102, 102, 23, 64, 103, 99, 102, 103, 103, 102, 101, 90, 65, 1, 104, 103, 104, 99, 102, 2749, 99, 2749, 115, 95, 371400430, 100, 4126025280, 66, 95, 102, 100, 102, 101, 103, 102, 101, 0, 102, 102, 102, 102, 100, 101, 70, 99, 103, 101, 100, 103, 103, 103, 100, 102, 103, 68, 40004, 101, 102, 102, 100, 95, 95, 140, 2520556167, 96, 77, 115, 95, 2834686210, 96, 2382952218, 77, 141, 37, 566230030, 91, 66, 91, 102, 92, 103, 102, 103, 103, 102, 0, 101, 102, 102, 102, 23, 103, 61, 92, 102, 102, 102, 102, 103, 103, 101, 23, 102, 69, 21, 101, 93, 101, 102, 103, 102, 103, 102, 103, 101, 23, 64, 101, 99, 102, 102, 102, 102, 101, 93, 65, 2, 104, 102, 104, 99, 101, 2862, 99, 2862, 115, 95, 262405981, 100, 3983476407, 66, 95, 101, 100, 101, 103, 101, 102, 103, 1, 103, 103, 103, 101, 100, 103, 70, 99, 103, 103, 100, 102, 103, 101, 100, 101, 103, 68, 40591, 101, 102, 103, 100, 95, 95, 140, 3041799263, 96, 77, 115, 95, 1847683821, 96, 1085613688, 77, 115, 95, 4253821147, 96, 1910745127, 77, 96, 97, 118, 6, 12, 3544495353, 94, 35848, 68, 39527, 102, 102, 101, 94, 21, 21, 115, 95, 2536345544, 96, 3359968532, 77, 140, 13285, 94, 68, 39601, 102, 102, 102, 94, 21, 21, 115, 95, 2850325175, 96, 2643202635, 77, 140, 3543, 4, 55, 140, 317, 94, 68, 26378, 103, 103, 101, 94, 21, 21, 115, 95, 696631177, 96, 502078805, 77, 98, 120, 16, 4079583454, 17, 154, 94, 44321, 68, 33119, 102, 102, 102, 94, 21, 21, 115, 95, 1936041141, 96, 3826070601, 77, 140, 15984, 21, 141, 7, 0, 106, 99, 140, 24622, 94, 68, 40902, 102, 102, 103, 94, 21, 21, 115, 95, 1494086149, 96, 241525465, 77, 100, 47, 15, 5949, 4, 98, 0, 0, 101, 140, 23555, 94, 66, 94, 103, 21, 101, 101, 101, 101, 101, 0, 103, 103, 101, 103, 21, 101, 115, 95, 3419536817, 96, 2076269389, 77, 140, 16296, 21, 102, 120, 16, 778833906, 17, 146, 94, 3402, 66, 94, 103, 21, 102, 103, 102, 102, 103, 2, 103, 102, 103, 101, 21, 102, 115, 95, 3025968324, 96, 2838756376, 77, 80, 42, 12, 15, 4, 14, 16, 17, 18, 118, 13, 39, 0, 94, 9909, 68, 49947, 103, 102, 102, 94, 21, 21, 115, 95, 1883488286, 96, 3845083362, 77, 61, 9, 101, 101, 101, 101, 101, 101, 101, 5, 103, 140, 3213, 4, 55, 140, 8025, 94, 68, 18658, 101, 103, 101, 94, 21, 21, 115, 95, 3339293127, 96, 2020146971, 77, 55, 140, 36888, 94, 66, 94, 101, 21, 102, 102, 103, 103, 102, 0, 103, 102, 102, 103, 21, 102, 115, 95, 3631270487, 96, 2367703723, 77, 136, 21, 19844, 16, 1677416180, 17, 164, 18, 7, 140, 18909, 94, 66, 94, 103, 21, 101, 101, 101, 101, 101, 1, 101, 101, 101, 102, 21, 101, 115, 95, 902665145, 96, 698679621, 77, 140, 4740, 21, 81, 140, 201, 94, 66, 94, 103, 21, 103, 103, 101, 101, 103, 2, 103, 103, 103, 101, 21, 101, 115, 95, 2965847502, 96, 2762644242, 77, 103, 104, 140, 19367, 94, 66, 94, 102, 21, 103, 102, 101, 101, 103, 1, 101, 103, 102, 101, 21, 101, 115, 95, 1918562305, 96, 3811991773, 77, 140, 3436, 21, 105, 55, 115, 4, 3000, 94, 7974, 66, 94, 101, 21, 102, 102, 102, 102, 102, 0, 102, 102, 102, 103, 21, 102, 115, 95, 601063739, 96, 331288519, 77, 71, 106, 0, 6, 101, 101, 101, 101, 101, 64, 101, 99, 101, 102, 101, 103, 101, 106, 65, 1, 104, 101, 104, 99, 102, 3329, 99, 3329, 62, 102, 103, 99, 101, 101, 101, 102, 102, 102, 103, 103, 99, 101, 115, 95, 407541319, 100, 597153744, 66, 95, 103, 100, 103, 101, 101, 102, 101, 2, 101, 101, 101, 102, 100, 101, 67, 103, 99, 102, 100, 100, 103, 101, 101, 103, 68, 1082, 101, 102, 102, 100, 95, 95, 140, 3648544458, 96, 77, 140, 6119, 94, 66, 94, 101, 21, 102, 102, 101, 102, 101, 0, 101, 102, 101, 102, 21, 102, 115, 95, 1903911148, 96, 3860262896, 77, 140, 34851, 94, 66, 94, 103, 21, 103, 102, 103, 102, 103, 2, 103, 101, 103, 102, 21, 102, 115, 95, 1081494682, 96, 4112108646, 77, 140, 0, 94, 68, 18095, 103, 103, 103, 94, 21, 21, 115, 95, 2967870992, 96, 2760474348, 77, 141, 5, 530117894, 106, 68, 54453, 101, 101, 103, 106, 9, 0, 55, 140, 21976, 94, 66, 94, 103, 21, 103, 102, 102, 103, 103, 1, 103, 103, 102, 103, 21, 103, 115, 95, 232102658, 96, 1102903774, 77, 42, 10, 15, 4, 14, 16, 17, 18, 106, 61, 106, 103, 101, 101, 103, 101, 101, 101, 5, 103, 140, 15044, 94, 68, 16241, 101, 101, 102, 94, 21, 21, 115, 95, 4251551915, 96, 1913194551, 77, 80, 42, 8, 15, 4, 14, 16, 17, 18, 44, 106, 7, 1, 8, 107, 140, 42722, 21, 81, 42, 95, 97, 2, 95, 96, 140, 0, 98, 108, 109, 124, 71, 40, 531375675, 72, 41, 1271561396, 73, 12, 3544495353, 70, 42, 3494486493, 47, 66, 9770, 2, 12, 0, 0, 110, 141, 43, 2283612997, 16, 61, 16, 70, 71, 71, 71, 71, 71, 71, 17, 70, 141, 12, 3544495353, 18, 61, 18, 70, 70, 70, 70, 71, 71, 71, 19, 71, 141, 44, 503379426, 20, 63, 71, 20, 71, 17, 71, 71, 70, 71, 71, 71, 21, 71, 70, 71, 71, 64, 70, 68, 70, 70, 70, 71, 71, 21, 65, 2, 72, 71, 72, 68, 71, 3648, 68, 3648, 115, 64, 2253028226, 69, 3884449667, 66, 64, 70, 69, 70, 71, 71, 71, 70, 2, 71, 70, 71, 70, 69, 71, 70, 68, 70, 71, 69, 70, 70, 71, 69, 70, 71, 68, 53520, 70, 71, 70, 69, 64, 64, 140, 2278123996, 65, 79, 141, 45, 130627398, 22, 61, 22, 71, 71, 70, 71, 70, 71, 70, 19, 71, 69, 17, 70, 23, 70, 71, 71, 70, 70, 71, 71, 70, 19, 64, 71, 68, 71, 71, 70, 71, 70, 23, 65, 0, 72, 71, 72, 68, 71, 3739, 68, 3739, 62, 70, 70, 68, 70, 70, 70, 70, 71, 71, 70, 71, 68, 70, 115, 64, 1628868867, 69, 4043013794, 66, 64, 70, 69, 70, 71, 70, 71, 71, 0, 70, 71, 71, 71, 69, 70, 67, 70, 68, 70, 69, 69, 70, 71, 70, 71, 68, 31961, 71, 71, 70, 69, 64, 64, 140, 3924546754, 65, 79, 115, 64, 2821421603, 65, 2445644946, 79, 141, 46, 3467710307, 24, 66, 24, 71, 25, 71, 70, 70, 71, 71, 1, 71, 71, 71, 71, 19, 71, 61, 25, 71, 71, 70, 71, 70, 71, 70, 19, 71, 69, 17, 71, 26, 71, 70, 70, 71, 70, 71, 70, 71, 19, 64, 71, 68, 71, 71, 71, 71, 71, 26, 65, 1, 72, 71, 72, 68, 70, 3865, 68, 3865, 115, 64, 862630622, 69, 3276752777, 66, 64, 71, 69, 71, 71, 71, 71, 71, 1, 71, 70, 70, 71, 69, 70, 70, 69, 71, 70, 69, 71, 70, 70, 68, 71, 70, 68, 1670, 71, 71, 71, 69, 64, 64, 140, 4131603399, 65, 79, 115, 64, 3423154448, 65, 1398517156, 79, 141, 47, 1034163869, 27, 68, 8440, 71, 71, 71, 27, 28, 19, 61, 28, 71, 71, 71, 70, 70, 71, 70, 19, 71, 69, 17, 71, 29, 71, 71, 71, 71, 70, 70, 70, 71, 19, 64, 70, 68, 70, 70, 71, 70, 71, 29, 65, 0, 72, 71, 72, 68, 70, 3970, 68, 3970, 115, 64, 2973982101, 69, 2172008910, 66, 64, 71, 69, 70, 70, 71, 71, 70, 1, 71, 70, 70, 70, 69, 71, 70, 68, 70, 71, 69, 71, 71, 71, 69, 70, 70, 68, 44163, 71, 71, 71, 69, 64, 64, 140, 1668592627, 65, 79, 115, 64, 1151935344, 65, 237351565, 79, 141, 48, 197605358, 30, 66, 30, 70, 31, 70, 70, 70, 70, 71, 0, 70, 70, 70, 71, 19, 70, 61, 31, 71, 71, 70, 70, 70, 70, 71, 19, 71, 69, 17, 70, 32, 71, 71, 70, 70, 70, 71, 70, 70, 19, 64, 70, 68, 70, 71, 70, 71, 70, 32, 65, 1, 72, 70, 72, 68, 70, 4083, 68, 4083, 62, 71, 71, 68, 70, 71, 70, 71, 71, 70, 70, 70, 68, 70, 115, 64, 1690620562, 69, 4108836960, 66, 64, 71, 69, 70, 70, 71, 70, 71, 1, 71, 70, 70, 70, 69, 70, 67, 71, 68, 70, 69, 69, 70, 70, 71, 71, 68, 58222, 71, 71, 71, 69, 64, 64, 140, 3332411051, 65, 79, 115, 64, 1177715376, 65, 3465857456, 79, 141, 49, 4038433615, 33, 68, 40874, 71, 71, 70, 33, 34, 19, 61, 34, 71, 71, 70, 70, 71, 71, 70, 19, 71, 69, 17, 71, 35, 70, 71, 70, 70, 70, 70, 71, 71, 19, 64, 71, 68, 71, 70, 70, 70, 71, 35, 65, 2, 72, 71, 72, 68, 71, 4201, 68, 4201, 115, 64, 1392390521, 69, 3806521252, 66, 64, 71, 69, 70, 71, 70, 70, 70, 2, 71, 71, 70, 70, 69, 70, 70, 68, 70, 70, 69, 70, 70, 70, 69, 71, 71, 68, 14367, 71, 71, 71, 69, 64, 64, 140, 2851916033, 65, 79, 115, 64, 1148316013, 65, 3299308216, 79, 141, 50, 253684191, 36, 66, 36, 71, 37, 70, 70, 70, 70, 71, 0, 70, 70, 70, 71, 19, 70, 61, 37, 71, 71, 70, 71, 71, 70, 70, 19, 71, 69, 17, 70, 38, 70, 71, 70, 71, 70, 70, 70, 70, 19, 64, 70, 68, 70, 71, 71, 70, 71, 38, 65, 2, 72, 71, 72, 68, 71, 4314, 68, 4314, 115, 64, 2287202533, 69, 1481126989, 66, 64, 71, 69, 71, 71, 70, 70, 71, 0, 70, 70, 71, 70, 69, 71, 70, 68, 70, 71, 69, 70, 71, 71, 69, 70, 70, 68, 28222, 71, 71, 70, 69, 64, 64, 140, 3227214583, 65, 79, 115, 64, 2162711234, 65, 4274488680, 79, 141, 51, 4007645834, 39, 68, 6766, 71, 70, 70, 39, 40, 19, 61, 40, 70, 70, 70, 70, 70, 70, 70, 19, 70, 69, 17, 70, 41, 70, 70, 70, 70, 70, 71, 70, 70, 19, 64, 70, 68, 70, 70, 70, 71, 70, 41, 65, 1, 72, 70, 72, 68, 71, 4419, 68, 4419, 62, 70, 70, 68, 70, 71, 70, 70, 70, 70, 70, 70, 68, 70, 115, 64, 1217647920, 69, 3603177595, 66, 64, 71, 69, 71, 70, 71, 70, 70, 0, 70, 71, 71, 71, 69, 70, 67, 70, 69, 71, 69, 68, 70, 70, 71, 71, 68, 55687, 71, 70, 70, 69, 64, 64, 140, 777404480, 65, 79, 115, 64, 1780722181, 65, 1070924917, 79, 141, 52, 4210248280, 42, 66, 42, 71, 43, 70, 70, 70, 70, 71, 0, 70, 70, 70, 71, 19, 70, 61, 43, 70, 70, 71, 71, 71, 71, 71, 19, 71, 69, 17, 71, 44, 71, 71, 71, 70, 70, 71, 70, 71, 19, 64, 70, 68, 71, 71, 71, 70, 70, 44, 65, 0, 72, 71, 72, 68, 70, 4545, 68, 4545, 62, 71, 70, 68, 71, 70, 71, 70, 71, 70, 71, 71, 68, 70, 115, 64, 1482196984, 69, 647528218, 66, 64, 70, 69, 71, 70, 71, 70, 70, 0, 71, 70, 71, 70, 69, 71, 67, 71, 68, 71, 69, 69, 71, 71, 70, 70, 68, 30546, 70, 71, 71, 69, 64, 64, 140, 4057954204, 65, 79, 115, 64, 3821126772, 65, 2703261350, 79, 141, 53, 2526324143, 45, 68, 15707, 70, 71, 70, 45, 46, 19, 61, 46, 70, 70, 70, 71, 71, 70, 71, 19, 71, 69, 17, 71, 47, 71, 71, 71, 71, 71, 71, 71, 71, 19, 64, 70, 68, 70, 71, 71, 71, 71, 47, 65, 1, 72, 71, 72, 68, 70, 4663, 68, 4663, 62, 70, 70, 68, 70, 70, 71, 70, 71, 70, 71, 70, 68, 70, 115, 64, 3588006586, 69, 3856679979, 66, 64, 71, 69, 71, 71, 70, 71, 70, 0, 71, 71, 70, 71, 69, 71, 67, 70, 68, 71, 69, 69, 71, 70, 70, 70, 68, 44822, 71, 70, 71, 69, 64, 64, 140, 2146074737, 65, 79, 115, 64, 1573741083, 65, 319418437, 79, 141, 54, 2373692470, 48, 66, 48, 71, 49, 71, 71, 71, 71, 71, 2, 71, 71, 71, 71, 19, 71, 61, 49, 70, 71, 71, 71, 71, 71, 71, 19, 70, 69, 17, 70, 50, 71, 70, 70, 71, 71, 70, 70, 70, 19, 64, 71, 68, 70, 71, 71, 71, 71, 50, 65, 2, 72, 70, 72, 68, 70, 4789, 68, 4789, 115, 64, 770123821, 69, 1038813088, 66, 64, 71, 69, 71, 70, 70, 71, 71, 0, 71, 70, 71, 71, 69, 70, 70, 69, 70, 70, 69, 70, 71, 71, 68, 70, 70, 68, 56785, 71, 70, 70, 69, 64, 64, 140, 1844531187, 65, 79, 115, 64, 2520303781, 65, 4071348398, 79, 141, 55, 2106989333, 51, 68, 26692, 70, 70, 70, 51, 52, 19, 61, 52, 71, 70, 71, 71, 70, 70, 71, 19, 70, 69, 17, 70, 53, 70, 71, 71, 70, 71, 71, 71, 70, 19, 64, 71, 68, 70, 70, 70, 70, 70, 53, 65, 1, 72, 70, 72, 68, 71, 4894, 68, 4894, 62, 70, 70, 68, 71, 70, 70, 71, 71, 70, 71, 70, 68, 70, 115, 64, 755402349, 69, 4244300708, 66, 64, 70, 69, 71, 70, 71, 71, 70, 0, 70, 71, 70, 70, 69, 70, 67, 70, 68, 71, 69, 69, 71, 70, 70, 70, 68, 52567, 70, 70, 70, 69, 64, 64, 140, 3654888989, 65, 79, 115, 64, 2837811621, 65, 482967915, 79, 141, 56, 2461306621, 54, 66, 54, 71, 55, 71, 71, 71, 71, 71, 0, 71, 71, 71, 70, 19, 71, 61, 55, 70, 71, 70, 71, 70, 71, 71, 19, 70, 69, 17, 71, 56, 70, 70, 70, 70, 70, 71, 70, 71, 19, 64, 70, 68, 71, 71, 70, 71, 71, 56, 65, 1, 72, 71, 72, 68, 70, 5020, 68, 5020, 115, 64, 2761140749, 69, 3029812618, 66, 64, 71, 69, 70, 70, 71, 71, 71, 1, 70, 70, 70, 71, 69, 71, 70, 69, 70, 70, 69, 71, 71, 70, 68, 71, 71, 68, 32799, 70, 70, 70, 69, 64, 64, 140, 2638726469, 65, 79, 115, 64, 4251109856, 65, 2705271968, 79, 141, 57, 1515374117, 57, 68, 2819, 71, 71, 71, 57, 58, 19, 61, 58, 70, 71, 71, 71, 71, 71, 71, 19, 71, 69, 17, 71, 59, 70, 71, 70, 70, 71, 71, 71, 71, 19, 64, 71, 68, 71, 71, 70, 71, 70, 59, 65, 1, 72, 71, 72, 68, 71, 5125, 68, 5125, 115, 64, 1586982923, 69, 1855674002, 66, 64, 71, 69, 70, 71, 71, 70, 71, 2, 70, 71, 70, 70, 69, 71, 70, 69, 70, 71, 69, 71, 71, 70, 68, 71, 70, 68, 9034, 70, 71, 70, 69, 64, 64, 140, 2164686245, 65, 79, 115, 64, 2032614618, 65, 3485239832, 79, 141, 58, 382462478, 60, 68, 13190, 71, 71, 71, 60, 61, 19, 61, 61, 71, 70, 71, 70, 71, 71, 71, 19, 70, 69, 17, 70, 62, 70, 70, 71, 70, 70, 70, 70, 70, 19, 64, 71, 68, 70, 70, 70, 71, 70, 62, 65, 2, 72, 70, 72, 68, 71, 5230, 68, 5230, 115, 64, 2576117048, 69, 1774119238, 66, 64, 70, 69, 71, 70, 70, 70, 70, 1, 70, 70, 70, 70, 69, 70, 70, 68, 70, 70, 69, 70, 71, 71, 69, 70, 70, 68, 20102, 71, 70, 71, 69, 64, 64, 140, 3585602605, 65, 79, 115, 64, 4207548604, 65, 33478914, 79, 115, 64, 833457348, 65, 2563220114, 79, 140, 17149, 63, 71, 63, 17, 63, 70, 70, 71, 71, 71, 64, 71, 68, 70, 70, 70, 70, 70, 63, 65, 1, 72, 71, 72, 68, 71, 5317, 68, 5317, 115, 64, 1030109612, 69, 1195102534, 66, 64, 70, 69, 71, 71, 71, 70, 71, 0, 70, 71, 70, 70, 69, 71, 70, 69, 70, 70, 69, 70, 70, 71, 68, 70, 71, 68, 3770, 71, 70, 71, 69, 64, 64, 140, 178412359, 65, 79, 140, 3470, 63, 68, 18519, 71, 71, 71, 63, 17, 17, 115, 64, 633854783, 65, 3962725021, 79, 140, 17749, 63, 66, 63, 71, 17, 70, 71, 71, 71, 71, 2, 71, 71, 71, 71, 17, 71, 115, 64, 2607914475, 65, 2135359418, 79, 68, 47362, 71, 71, 70, 13, 74, 5, 43, 12, 3, 11, 1, 74, 118, 14, 59, 3305569827, 63, 13258, 66, 63, 70, 17, 71, 70, 71, 71, 70, 2, 71, 71, 70, 71, 17, 70, 115, 64, 354597596, 65, 4061758811, 79, 139, 2, 74, 60, 2753607229, 42, 6, 74, 1, 0, 140, 46415, 63, 66, 63, 71, 17, 71, 71, 71, 71, 71, 2, 70, 71, 71, 71, 17, 71, 115, 64, 3142669890, 65, 2301564549, 79, 61, 6, 70, 70, 71, 70, 71, 71, 71, 3, 71, 141, 61, 3956692895, 74, 61, 74, 70, 71, 70, 70, 70, 70, 71, 4, 70, 140, 33424, 63, 68, 18120, 70, 70, 71, 63, 17, 17, 115, 64, 3231910470, 65, 1952984381, 79, 125, 7, 62, 3213211998, 74, 63, 343371583, 111, 140, 10993, 63, 66, 63, 70, 17, 70, 70, 71, 71, 70, 2, 71, 70, 70, 70, 17, 71, 115, 64, 2681255140, 65, 770578055, 79, 68, 56788, 70, 71, 70, 9, 74, 4, 61, 74, 70, 71, 70, 71, 70, 70, 71, 4, 71, 140, 10052, 63, 66, 63, 71, 17, 70, 71, 70, 70, 70, 2, 71, 71, 70, 70, 17, 71, 115, 64, 4220850886, 65, 1137430, 79, 112, 141, 64, 1738749825, 74, 68, 324, 70, 70, 70, 74, 75, 5, 61, 75, 71, 71, 70, 70, 71, 70, 70, 5, 71, 140, 4271, 63, 68, 28316, 70, 71, 70, 63, 17, 17, 115, 64, 1376486339, 65, 1635123340, 79, 122, 74, 7, 0, 74, 119, 74, 65, 153373723, 75, 3, 74, 71, 74, 5, 75, 71, 70, 71, 71, 71, 64, 70, 68, 71, 70, 71, 71, 70, 74, 65, 1, 72, 70, 72, 68, 71, 5681, 68, 5681, 62, 71, 70, 68, 70, 70, 71, 71, 70, 70, 70, 71, 68, 70, 115, 64, 3103267204, 69, 43974562, 66, 64, 71, 69, 71, 70, 71, 71, 71, 0, 71, 70, 70, 71, 69, 70, 67, 70, 69, 70, 69, 68, 70, 71, 70, 70, 68, 43576, 70, 71, 71, 69, 64, 64, 140, 4165854191, 65, 79, 140, 16205, 63, 66, 63, 70, 17, 70, 70, 70, 70, 70, 0, 70, 71, 70, 70, 17, 70, 115, 64, 597355071, 65, 1692591108, 79, 140, 16784, 63, 68, 24737, 70, 71, 71, 63, 17, 17, 115, 64, 3031155297, 65, 3898944862, 79, 141, 12, 3544495353, 74, 61, 74, 71, 70, 70, 71, 70, 71, 70, 5, 70, 140, 5014, 63, 66, 63, 71, 17, 70, 71, 71, 71, 70, 1, 71, 70, 71, 71, 17, 71, 115, 64, 3263429498, 65, 2442081049, 79, 72, 71, 70, 71, 74, 14, 12, 71, 73, 74, 75, 71, 70, 70, 71, 71, 10, 70, 70, 70, 43, 9, 7, 8, 1, 75, 140, 3475, 63, 66, 63, 71, 17, 70, 70, 71, 70, 70, 1, 71, 71, 70, 70, 17, 71, 115, 64, 1690086458, 65, 1832704752, 79, 119, 74, 66, 3246909318, 75, 3, 74, 43, 10, 3, 75, 1, 5, 140, 17083, 63, 68, 21521, 70, 70, 71, 63, 17, 17, 115, 64, 752848549, 65, 1596153649, 79, 137, 74, 66, 3246909318, 11, 3, 74, 13, 5, 530117894, 63, 360, 66, 63, 71, 17, 70, 70, 70, 71, 71, 0, 71, 71, 71, 70, 17, 70, 115, 64, 3753741133, 65, 2618229633, 79, 42, 64, 66, 2, 65, 64, 119, 67, 67, 1654912902, 64, 64, 67, 113, 124, 93, 68, 2419621500, 95, 12, 3544495353, 92, 69, 3373611847, 94, 70, 3103496811, 47, 89, 10097, 2, 12, 0, 0, 133, 20, 21, 71, 1580661460, 61, 21, 93, 92, 92, 93, 92, 93, 92, 22, 92, 141, 12, 3544495353, 23, 61, 23, 93, 92, 92, 93, 93, 93, 92, 24, 93, 141, 72, 2697282354, 25, 63, 92, 25, 93, 22, 92, 92, 92, 92, 93, 93, 26, 93, 92, 92, 92, 64, 92, 90, 92, 93, 92, 92, 92, 26, 65, 2, 94, 93, 94, 90, 92, 6039, 90, 6039, 62, 93, 93, 90, 92, 93, 92, 92, 93, 93, 93, 93, 90, 93, 115, 87, 3555588342, 91, 1124939217, 66, 87, 92, 91, 92, 92, 93, 92, 93, 2, 93, 93, 92, 92, 91, 93, 67, 93, 90, 92, 91, 91, 93, 93, 92, 93, 68, 28164, 92, 92, 92, 91, 87, 87, 140, 781345408, 88, 78, 141, 73, 1085256308, 27, 61, 27, 92, 92, 92, 92, 92, 92, 92, 24, 93, 69, 22, 92, 28, 93, 93, 93, 93, 92, 92, 92, 92, 24, 64, 92, 90, 93, 92, 92, 92, 92, 28, 65, 1, 94, 92, 94, 90, 93, 6143, 90, 6143, 62, 92, 93, 90, 92, 93, 93, 92, 93, 93, 93, 93, 90, 93, 115, 87, 3984880668, 91, 3235694210, 66, 87, 92, 91, 92, 93, 93, 92, 92, 1, 92, 93, 93, 92, 91, 92, 67, 92, 91, 92, 91, 90, 93, 93, 93, 93, 68, 49031, 93, 93, 92, 91, 87, 87, 140, 2081703458, 88, 78, 115, 87, 452861197, 88, 625092506, 78, 141, 74, 3290783121, 29, 68, 43257, 92, 93, 92, 29, 30, 24, 61, 30, 93, 93, 93, 92, 92, 93, 92, 24, 92, 69, 22, 92, 31, 92, 92, 93, 92, 92, 92, 93, 92, 24, 64, 92, 90, 92, 93, 92, 93, 93, 31, 65, 2, 94, 92, 94, 90, 92, 6261, 90, 6261, 62, 92, 92, 90, 93, 93, 93, 92, 92, 92, 93, 92, 90, 93, 115, 87, 3549079835, 91, 2799893377, 66, 87, 92, 91, 92, 92, 92, 93, 92, 1, 92, 93, 92, 93, 91, 93, 67, 92, 90, 92, 91, 91, 92, 92, 93, 92, 68, 5417, 92, 92, 93, 91, 87, 87, 140, 1581207433, 88, 78, 115, 87, 2789250476, 88, 708208526, 78, 141, 75, 2410024525, 32, 66, 32, 93, 33, 92, 92, 92, 92, 92, 1, 93, 93, 92, 92, 24, 92, 61, 33, 92, 92, 93, 93, 93, 93, 92, 24, 92, 69, 22, 93, 34, 92, 92, 92, 93, 92, 92, 92, 93, 24, 64, 92, 90, 93, 93, 92, 93, 92, 34, 65, 0, 94, 92, 94, 90, 93, 6387, 90, 6387, 115, 87, 846031325, 91, 96844867, 66, 87, 92, 91, 92, 92, 92, 93, 92, 0, 92, 93, 93, 93, 91, 92, 70, 90, 92, 93, 91, 93, 93, 92, 91, 92, 93, 68, 48008, 93, 93, 92, 91, 87, 87, 140, 2548973400, 88, 78, 115, 87, 1798143759, 88, 1287982065, 78, 141, 76, 3679173802, 35, 68, 47565, 92, 93, 92, 35, 36, 24, 61, 36, 92, 92, 92, 92, 92, 92, 93, 24, 92, 69, 22, 92, 37, 93, 93, 92, 92, 92, 92, 93, 92, 24, 64, 92, 90, 92, 92, 92, 93, 92, 37, 65, 0, 94, 93, 94, 90, 93, 6492, 90, 6492, 62, 93, 92, 90, 92, 93, 92, 92, 92, 93, 93, 92, 90, 93, 115, 87, 1768071269, 91, 1018884811, 66, 87, 92, 91, 92, 92, 93, 93, 92, 2, 92, 93, 92, 93, 91, 93, 67, 93, 90, 93, 91, 91, 92, 93, 92, 93, 68, 27780, 93, 93, 93, 91, 87, 87, 140, 3951165894, 88, 78, 115, 87, 997178974, 88, 2465090783, 78, 141, 77, 730616477, 38, 66, 38, 93, 39, 93, 93, 92, 93, 93, 1, 93, 92, 93, 93, 24, 92, 61, 39, 92, 93, 93, 92, 92, 92, 93, 24, 92, 69, 22, 93, 40, 92, 93, 93, 92, 93, 93, 92, 92, 24, 64, 92, 90, 92, 92, 92, 93, 92, 40, 65, 2, 94, 92, 94, 90, 92, 6618, 90, 6618, 115, 87, 1316399857, 91, 567213399, 66, 87, 92, 91, 92, 93, 93, 93, 93, 2, 92, 92, 93, 93, 91, 92, 70, 91, 93, 92, 91, 93, 92, 93, 90, 93, 92, 68, 38426, 93, 92, 93, 91, 87, 87, 140, 2642180203, 88, 78, 115, 87, 2780445275, 88, 2923388467, 78, 141, 78, 1391259310, 41, 66, 41, 93, 42, 92, 93, 92, 92, 93, 0, 93, 92, 93, 92, 24, 93, 61, 42, 93, 93, 93, 93, 93, 93, 93, 24, 93, 69, 22, 92, 43, 92, 93, 93, 92, 93, 92, 92, 92, 24, 64, 93, 90, 92, 92, 92, 92, 93, 43, 65, 1, 94, 92, 94, 90, 92, 6731, 90, 6731, 62, 92, 93, 90, 92, 93, 92, 92, 93, 92, 92, 92, 90, 92, 115, 87, 770836248, 91, 21649790, 66, 87, 92, 91, 92, 92, 92, 92, 93, 2, 92, 93, 92, 93, 91, 93, 67, 92, 91, 93, 91, 90, 92, 92, 93, 92, 68, 21959, 93, 92, 92, 91, 87, 87, 140, 3945110834, 88, 78, 115, 87, 3959518427, 88, 1015597961, 78, 141, 79, 1297697474, 44, 68, 36316, 92, 93, 92, 44, 45, 24, 61, 45, 92, 92, 92, 93, 92, 92, 93, 24, 93, 69, 22, 92, 46, 92, 93, 92, 92, 92, 93, 92, 92, 24, 64, 92, 90, 92, 93, 93, 93, 92, 46, 65, 2, 94, 92, 94, 90, 92, 6849, 90, 6849, 62, 93, 93, 90, 93, 93, 92, 93, 92, 92, 93, 92, 90, 92, 115, 87, 2597613746, 91, 1848427288, 66, 87, 93, 91, 92, 93, 92, 92, 92, 0, 92, 93, 93, 93, 91, 92, 67, 93, 90, 93, 91, 91, 92, 93, 93, 93, 68, 31204, 93, 93, 93, 91, 87, 87, 140, 533635502, 88, 78, 115, 87, 4239639456, 88, 220283529, 78, 141, 80, 2451659418, 47, 68, 45631, 92, 92, 92, 47, 48, 24, 61, 48, 92, 92, 92, 92, 92, 92, 92, 24, 92, 69, 22, 92, 49, 92, 92, 92, 93, 92, 93, 92, 93, 24, 64, 92, 90, 92, 93, 92, 93, 93, 49, 65, 0, 94, 93, 94, 90, 93, 6967, 90, 6967, 62, 93, 92, 90, 93, 92, 93, 93, 93, 93, 92, 93, 90, 93, 115, 87, 1349631713, 91, 600445255, 66, 87, 93, 91, 92, 93, 93, 93, 92, 2, 93, 92, 92, 92, 91, 92, 67, 92, 91, 92, 91, 90, 93, 93, 92, 93, 68, 32116, 92, 92, 92, 91, 87, 87, 140, 2480506085, 88, 78, 115, 87, 2957412824, 88, 1212861945, 78, 141, 81, 3452384568, 50, 66, 50, 92, 51, 92, 92, 93, 92, 92, 1, 92, 92, 92, 92, 24, 92, 61, 51, 93, 92, 92, 93, 92, 93, 92, 24, 93, 69, 22, 92, 52, 92, 93, 92, 92, 93, 92, 93, 92, 24, 64, 93, 90, 92, 93, 93, 93, 92, 52, 65, 1, 94, 93, 94, 90, 92, 7093, 90, 7093, 62, 92, 93, 90, 92, 93, 92, 92, 93, 92, 93, 93, 90, 93, 115, 87, 3733226263, 91, 2984039805, 66, 87, 92, 91, 92, 93, 92, 92, 92, 1, 92, 93, 92, 93, 91, 93, 67, 92, 90, 93, 91, 91, 92, 93, 92, 92, 68, 28163, 92, 93, 93, 91, 87, 87, 140, 4236693645, 88, 78, 115, 87, 4082865118, 88, 1086509251, 78, 141, 82, 2350769845, 53, 68, 47705, 93, 93, 92, 53, 54, 24, 61, 54, 92, 93, 93, 93, 92, 92, 92, 24, 93, 69, 22, 93, 55, 92, 93, 93, 92, 92, 93, 92, 93, 24, 64, 93, 90, 92, 93, 92, 93, 93, 55, 65, 2, 94, 92, 94, 90, 93, 7211, 90, 7211, 115, 87, 3801511648, 91, 3052325190, 66, 87, 93, 91, 93, 93, 93, 92, 92, 2, 92, 92, 93, 93, 91, 93, 70, 90, 92, 93, 91, 92, 92, 93, 91, 92, 93, 68, 37817, 92, 92, 92, 91, 87, 87, 140, 1963211709, 88, 78, 115, 87, 76557409, 88, 3075799227, 78, 141, 83, 2277058862, 56, 68, 28157, 93, 92, 93, 56, 57, 24, 61, 57, 93, 93, 92, 92, 93, 92, 93, 24, 92, 69, 22, 92, 58, 93, 92, 92, 92, 93, 92, 93, 93, 24, 64, 93, 90, 93, 93, 93, 93, 93, 58, 65, 1, 94, 93, 94, 90, 93, 7316, 90, 7316, 62, 93, 92, 90, 92, 93, 93, 92, 93, 92, 93, 93, 90, 93, 115, 87, 2744035576, 91, 1994849118, 66, 87, 93, 91, 93, 92, 92, 92, 92, 2, 92, 93, 93, 93, 91, 92, 67, 92, 90, 92, 91, 91, 93, 93, 92, 93, 68, 50224, 92, 93, 92, 91, 87, 87, 140, 2905922459, 88, 78, 115, 87, 1820232641, 88, 2642496906, 78, 141, 84, 3502745225, 59, 66, 59, 93, 60, 92, 92, 92, 93, 93, 2, 93, 93, 93, 93, 24, 93, 61, 60, 93, 93, 92, 92, 92, 92, 93, 24, 93, 69, 22, 92, 61, 93, 93, 93, 93, 93, 93, 93, 92, 24, 64, 93, 90, 93, 93, 92, 93, 92, 61, 65, 1, 94, 93, 94, 90, 92, 7442, 90, 7442, 115, 87, 631432949, 91, 4177213787, 66, 87, 92, 91, 92, 92, 93, 92, 92, 0, 92, 92, 92, 93, 91, 93, 70, 90, 93, 93, 91, 93, 93, 92, 91, 93, 93, 68, 3153, 92, 92, 93, 91, 87, 87, 140, 888948703, 88, 78, 115, 87, 4048277200, 88, 4078127675, 78, 141, 85, 2623980491, 62, 68, 58301, 93, 93, 93, 62, 63, 24, 61, 63, 92, 93, 92, 93, 92, 92, 93, 24, 93, 69, 22, 93, 64, 93, 92, 92, 93, 92, 92, 93, 92, 24, 64, 93, 90, 93, 93, 93, 93, 93, 64, 65, 0, 94, 93, 94, 90, 92, 7547, 90, 7547, 115, 87, 2601268663, 91, 1852082205, 66, 87, 93, 91, 93, 93, 93, 93, 93, 2, 92, 93, 93, 92, 91, 92, 70, 90, 92, 92, 91, 92, 92, 92, 91, 92, 93, 68, 55173, 92, 92, 93, 91, 87, 87, 140, 512330150, 88, 78, 115, 87, 2344196709, 88, 2217652773, 78, 141, 86, 3326858075, 65, 68, 52765, 92, 92, 93, 65, 66, 24, 61, 66, 93, 92, 92, 93, 93, 93, 93, 24, 92, 69, 22, 92, 67, 93, 92, 92, 92, 93, 92, 92, 92, 24, 64, 92, 90, 92, 92, 92, 93, 93, 67, 65, 2, 94, 92, 94, 90, 93, 7652, 90, 7652, 62, 93, 93, 90, 92, 92, 92, 93, 93, 92, 92, 92, 90, 93, 115, 87, 3939953783, 91, 3190767325, 66, 87, 93, 91, 92, 93, 92, 92, 93, 1, 92, 92, 93, 93, 91, 93, 67, 92, 91, 93, 91, 90, 92, 93, 93, 93, 68, 63736, 93, 93, 92, 91, 87, 87, 140, 3309551452, 88, 78, 115, 87, 1848551748, 88, 1990868618, 78, 141, 87, 4077623314, 68, 66, 68, 92, 69, 92, 93, 93, 92, 93, 2, 92, 92, 93, 92, 24, 92, 61, 69, 93, 92, 92, 93, 92, 92, 93, 24, 92, 69, 22, 93, 70, 93, 93, 93, 92, 92, 93, 92, 93, 24, 64, 93, 90, 92, 92, 92, 93, 92, 70, 65, 0, 94, 92, 94, 90, 93, 7778, 90, 7778, 62, 93, 92, 90, 93, 92, 92, 92, 93, 92, 92, 92, 90, 93, 115, 87, 4005364659, 91, 3256178201, 66, 87, 93, 91, 93, 93, 92, 93, 92, 1, 93, 93, 93, 93, 91, 92, 67, 93, 90, 92, 91, 91, 93, 92, 92, 93, 68, 26360, 92, 92, 92, 91, 87, 87, 140, 3434173534, 88, 78, 115, 87, 3900820779, 88, 1745066127, 78, 141, 88, 3112548740, 71, 66, 71, 92, 72, 92, 93, 92, 92, 93, 1, 92, 92, 93, 92, 24, 92, 61, 72, 93, 93, 93, 92, 92, 92, 92, 24, 92, 69, 22, 92, 73, 92, 93, 92, 92, 92, 93, 93, 92, 24, 64, 93, 90, 93, 92, 92, 92, 93, 73, 65, 0, 94, 92, 94, 90, 93, 7904, 90, 7904, 62, 93, 93, 90, 92, 92, 92, 93, 93, 93, 92, 93, 90, 93, 115, 87, 2884794703, 91, 2135608245, 66, 87, 93, 91, 92, 93, 92, 93, 93, 1, 93, 93, 93, 92, 91, 93, 67, 93, 90, 93, 91, 91, 92, 93, 92, 92, 68, 12035, 92, 92, 93, 91, 87, 87, 140, 1692938496, 88, 78, 115, 87, 2546502907, 88, 3932517411, 78, 141, 89, 4183579452, 74, 66, 74, 93, 75, 92, 92, 92, 92, 92, 2, 92, 92, 92, 92, 24, 92, 61, 75, 92, 92, 92, 92, 92, 93, 92, 24, 93, 69, 22, 93, 76, 93, 93, 92, 92, 92, 93, 93, 92, 24, 64, 93, 90, 93, 93, 92, 92, 93, 76, 65, 0, 94, 93, 94, 90, 92, 8030, 90, 8030, 115, 87, 1731590028, 91, 982403570, 66, 87, 93, 91, 92, 93, 92, 93, 93, 0, 92, 92, 93, 93, 91, 92, 70, 91, 92, 93, 91, 93, 93, 93, 90, 92, 92, 68, 21849, 92, 93, 93, 91, 87, 87, 140, 2482561284, 88, 78, 115, 87, 3257910612, 88, 2238972546, 78, 141, 90, 1627336464, 77, 68, 21594, 92, 92, 92, 77, 78, 24, 61, 78, 92, 92, 92, 92, 92, 92, 93, 24, 93, 69, 22, 93, 79, 93, 92, 93, 92, 92, 92, 93, 92, 24, 64, 93, 90, 93, 92, 92, 92, 93, 79, 65, 1, 94, 92, 94, 90, 92, 8135, 90, 8135, 62, 93, 93, 90, 93, 93, 93, 93, 92, 92, 92, 93, 90, 93, 115, 87, 3896733802, 91, 3147547344, 66, 87, 93, 91, 93, 93, 93, 93, 92, 2, 93, 92, 93, 92, 91, 93, 67, 92, 90, 93, 91, 91, 93, 93, 93, 92, 68, 48904, 92, 93, 92, 91, 87, 87, 140, 1558876796, 88, 78, 115, 87, 227046995, 88, 1949061487, 78, 141, 91, 4009190878, 80, 66, 80, 92, 81, 92, 92, 92, 92, 92, 2, 92, 92, 92, 93, 24, 93, 61, 81, 92, 92, 92, 92, 92, 92, 92, 24, 92, 69, 22, 92, 82, 92, 93, 92, 92, 92, 93, 92, 92, 24, 64, 93, 90, 93, 93, 93, 92, 93, 82, 65, 0, 94, 93, 94, 90, 93, 8261, 90, 8261, 62, 93, 93, 90, 92, 93, 92, 92, 93, 92, 93, 92, 90, 93, 115, 87, 372099641, 91, 3917880479, 66, 87, 93, 91, 93, 92, 93, 93, 93, 2, 93, 92, 93, 92, 91, 92, 67, 93, 91, 92, 91, 90, 93, 93, 93, 92, 68, 36585, 93, 92, 92, 91, 87, 87, 140, 3754583067, 88, 78, 115, 87, 4001897698, 88, 4122026400, 78, 141, 92, 3088560760, 83, 66, 83, 93, 84, 92, 92, 93, 92, 92, 0, 93, 93, 92, 93, 24, 92, 61, 84, 93, 93, 93, 93, 93, 93, 93, 24, 92, 69, 22, 92, 85, 93, 93, 93, 93, 93, 92, 92, 93, 24, 64, 93, 90, 92, 93, 92, 93, 93, 85, 65, 1, 94, 93, 94, 90, 92, 8387, 90, 8387, 115, 87, 3828029799, 91, 3078843341, 66, 87, 92, 91, 92, 92, 92, 93, 92, 0, 93, 92, 93, 92, 91, 93, 70, 90, 93, 93, 91, 93, 93, 93, 91, 92, 93, 68, 5400, 92, 93, 92, 91, 87, 87, 140, 2308654142, 88, 78, 115, 87, 3907749159, 88, 3983873162, 78, 115, 87, 4267773388, 88, 3121587882, 78, 61, 19, 93, 93, 93, 93, 93, 93, 92, 7, 92, 140, 6065, 86, 68, 10748, 93, 92, 93, 86, 22, 22, 115, 87, 2219358448, 88, 2379953606, 78, 71, 96, 7, 3, 92, 93, 93, 93, 93, 64, 92, 90, 92, 92, 92, 93, 92, 96, 65, 1, 94, 92, 94, 90, 93, 8499, 90, 8499, 115, 87, 1210805185, 91, 1469103896, 66, 87, 92, 91, 93, 92, 93, 93, 92, 1, 92, 92, 93, 93, 91, 93, 70, 90, 92, 93, 91, 92, 92, 92, 91, 93, 92, 68, 49325, 93, 93, 92, 91, 87, 87, 140, 610429965, 88, 78, 140, 2079, 86, 66, 86, 93, 22, 93, 92, 92, 93, 93, 1, 93, 93, 92, 93, 22, 92, 115, 87, 1842500659, 88, 1956250803, 78, 140, 6925, 86, 66, 86, 92, 22, 92, 92, 93, 92, 92, 1, 92, 92, 92, 92, 22, 92, 115, 87, 1752983397, 88, 570599585, 78, 140, 47946, 86, 71, 86, 22, 86, 92, 92, 93, 93, 93, 64, 93, 90, 92, 93, 93, 93, 92, 86, 65, 1, 94, 93, 94, 90, 92, 8624, 90, 8624, 62, 92, 92, 90, 92, 92, 93, 92, 92, 93, 92, 93, 90, 93, 115, 87, 3276151252, 91, 3967190451, 66, 87, 92, 91, 93, 92, 92, 93, 92, 1, 92, 92, 92, 92, 91, 93, 67, 93, 90, 92, 91, 91, 92, 92, 92, 93, 68, 44224, 92, 93, 92, 91, 87, 87, 140, 2377381292, 88, 78, 140, 51281, 86, 68, 4354, 93, 93, 93, 86, 22, 22, 115, 87, 2678092323, 88, 1191749059, 78, 140, 46717, 86, 68, 35251, 93, 93, 92, 86, 22, 22, 115, 87, 2434255701, 88, 1633969073, 78, 133, 5, 96, 61, 3956692895, 61, 96, 93, 93, 93, 92, 93, 93, 93, 6, 92, 140, 8002, 86, 66, 86, 92, 22, 92, 93, 92, 92, 93, 1, 92, 92, 92, 92, 22, 93, 115, 87, 126587993, 88, 1375695597, 78, 61, 18, 92, 93, 93, 93, 93, 92, 93, 6, 93, 140, 4336, 86, 68, 26471, 92, 93, 92, 86, 22, 22, 115, 87, 3500270703, 88, 37204023, 78, 73, 10, 96, 93, 93, 92, 93, 93, 9, 93, 92, 93, 61, 96, 92, 92, 92, 92, 92, 93, 93, 1, 92, 118, 11, 93, 1270104351, 86, 32524, 68, 3666, 92, 92, 92, 86, 22, 22, 115, 87, 521054974, 88, 425812120, 78, 140, 7831, 86, 71, 86, 22, 86, 92, 92, 92, 92, 92, 64, 92, 90, 92, 93, 92, 93, 93, 86, 65, 1, 94, 93, 94, 90, 93, 8858, 90, 8858, 62, 93, 92, 90, 93, 93, 93, 93, 93, 92, 93, 93, 90, 92, 115, 87, 842952195, 91, 1533991394, 66, 87, 93, 91, 93, 93, 93, 93, 93, 2, 93, 92, 93, 92, 91, 92, 67, 93, 91, 93, 91, 90, 93, 92, 93, 92, 68, 8350, 92, 92, 92, 91, 87, 87, 140, 3430953815, 88, 78, 140, 52772, 86, 68, 17185, 92, 92, 93, 86, 22, 22, 115, 87, 3029250228, 88, 2182120002, 78, 140, 47348, 86, 68, 29253, 93, 92, 93, 86, 22, 22, 115, 87, 4276177631, 88, 3965882567, 78, 74, 96, 93, 2, 11, 1, 93, 93, 93, 93, 75, 93, 93, 93, 12, 1, 96, 92, 92, 93, 92, 92, 92, 118, 13, 94, 249276301, 86, 24351, 66, 86, 92, 22, 92, 93, 92, 92, 93, 0, 93, 93, 92, 92, 22, 92, 115, 87, 1255193028, 88, 2136091186, 78, 141, 12, 3544495353, 96, 61, 96, 93, 92, 92, 92, 92, 93, 92, 7, 93, 140, 10850, 86, 68, 31662, 93, 92, 92, 86, 22, 22, 115, 87, 2150354095, 88, 3634416631, 78, 140, 59861, 86, 71, 86, 22, 86, 92, 92, 92, 93, 93, 64, 92, 90, 93, 93, 93, 92, 92, 86, 65, 0, 94, 93, 94, 90, 92, 9063, 90, 9063, 115, 87, 1927796853, 91, 2186095564, 66, 87, 92, 91, 92, 93, 92, 92, 93, 1, 93, 92, 92, 93, 91, 92, 70, 90, 92, 93, 91, 93, 92, 93, 91, 92, 92, 68, 22441, 93, 92, 93, 91, 87, 87, 140, 3070325965, 88, 78, 140, 44683, 86, 66, 86, 92, 22, 92, 92, 92, 92, 92, 1, 93, 93, 92, 93, 22, 93, 115, 87, 3921817640, 88, 2583372302, 78, 140, 20332, 86, 66, 86, 93, 22, 93, 93, 92, 93, 93, 0, 93, 92, 93, 92, 22, 93, 115, 87, 3906311739, 88, 3687805739, 78, 122, 96, 7, 0, 96, 140, 3168, 86, 66, 86, 92, 22, 93, 92, 93, 92, 92, 0, 92, 92, 92, 93, 22, 93, 115, 87, 2280081648, 88, 2856691142, 78, 140, 22583, 86, 68, 54987, 93, 92, 93, 86, 22, 22, 115, 87, 3085321418, 88, 3806575500, 78, 141, 95, 3045554691, 96, 68, 58635, 93, 93, 92, 96, 9, 1, 118, 10, 12, 3544495353, 86, 47432, 66, 86, 92, 22, 92, 92, 92, 92, 93, 0, 92, 93, 93, 92, 22, 92, 115, 87, 179372345, 88, 559453197, 78, 61, 7, 93, 93, 93, 93, 93, 93, 92, 96, 92, 141, 5, 530117894, 96, 68, 43509, 93, 93, 92, 96, 19, 7, 140, 8860, 86, 66, 86, 93, 22, 92, 93, 93, 92, 93, 1, 93, 93, 93, 93, 22, 93, 115, 87, 3312645427, 88, 1314223027, 78, 137, 96, 63, 343371583, 15, 14, 96, 16, 66, 3246909318, 86, 37396, 66, 86, 93, 22, 92, 93, 93, 93, 93, 0, 93, 92, 93, 92, 22, 92, 115, 87, 2426223503, 88, 2677148439, 78, 67, 92, 13, 93, 96, 12, 92, 92, 93, 92, 61, 96, 92, 92, 92, 93, 93, 92, 93, 8, 93, 143, 3213211998, 62, 14, 140, 49900, 86, 68, 37907, 92, 93, 92, 86, 22, 22, 115, 87, 2056672643, 88, 569227363, 78, 75, 92, 92, 92, 96, 17, 8, 93, 93, 92, 92, 93, 92, 43, 97, 14, 15, 1, 96, 68, 25314, 93, 92, 93, 97, 18, 6, 140, 2732, 86, 68, 28329, 92, 93, 92, 86, 22, 22, 115, 87, 3272407980, 88, 1096570570, 78, 142, 6, 144, 0, 16, 96, 68, 17341, 93, 92, 93, 7, 97, 2, 43, 17, 0, 96, 1, 97, 140, 29407, 86, 68, 39640, 92, 92, 92, 86, 22, 22, 115, 87, 3006047813, 88, 2101483457, 78, 42, 87, 89, 2, 88, 87, 114, 127, 6, 96, 169913266, 4, 97, 1445746322, 5, 98, 3706552353, 3, 76, 6, 5, 6, 38693, 7, 5, 6, 5, 5, 4, 4, 0, 6, 141, 12, 3544495353, 8, 74, 9, 4, 2, 8, 7, 6, 4, 4, 5, 61, 9, 5, 6, 4, 5, 4, 5, 6, 0, 5, 141, 99, 1204589306, 7, 75, 6, 6, 4, 8, 0, 7, 6, 4, 5, 5, 4, 5, 141, 12, 3544495353, 7, 74, 9, 5, 1, 7, 8, 5, 4, 4, 6, 61, 9, 6, 4, 6, 6, 4, 5, 5, 0, 4, 141, 100, 519630121, 7, 68, 65198, 4, 4, 5, 7, 8, 0, 141, 12, 3544495353, 7, 74, 9, 4, 0, 7, 8, 5, 5, 5, 5, 61, 9, 6, 5, 4, 5, 4, 4, 4, 0, 4, 141, 101, 3717331291, 7, 66, 7, 5, 8, 6, 6, 6, 6, 6, 0, 5, 6, 6, 4, 0, 6, 141, 12, 3544495353, 7, 74, 9, 5, 1, 7, 8, 5, 6, 5, 4, 61, 9, 4, 4, 5, 5, 5, 4, 4, 0, 5, 68, 14808, 5, 5, 6, 1, 7, 0, 141, 12, 3544495353, 8, 74, 9, 5, 2, 8, 7, 4, 4, 4, 5, 61, 9, 4, 6, 6, 4, 5, 4, 4, 0, 6, 126, 7, 102, 2212785524, 8, 103, 2749574982, 9, 7, 8, 10, 104, 2555947145, 11, 7, 9, 2, 0, 10, 141, 12, 3544495353, 7, 74, 8, 4, 2, 7, 11, 6, 4, 4, 4, 61, 8, 4, 6, 4, 4, 4, 6, 5, 0, 6, 141, 105, 1704232641, 7, 74, 8, 4, 2, 7, 0, 4, 6, 5, 4, 75, 4, 4, 6, 7, 0, 8, 6, 4, 4, 6, 6, 4, 141, 12, 3544495353, 8, 74, 9, 6, 2, 8, 7, 5, 5, 5, 5, 61, 9, 5, 4, 6, 5, 4, 6, 4, 0, 6, 48, 7, 1, 0, 142, 7, 122, 7, 7, 0, 7, 127, 4, 106, 359519542, 6, 107, 1290274898, 5, 108, 2040698413, 3, 141, 109, 3546688484, 7, 74, 8, 5, 2, 7, 1, 5, 4, 4, 5, 75, 4, 4, 5, 7, 1, 8, 5, 4, 5, 5, 6, 5, 141, 12, 3544495353, 8, 74, 9, 5, 0, 8, 7, 4, 5, 4, 5, 61, 9, 6, 4, 6, 4, 5, 4, 6, 1, 4, 76, 5, 5, 4, 28403, 7, 6, 4, 6, 4, 6, 6, 1, 4, 141, 12, 3544495353, 8, 74, 9, 4, 2, 8, 7, 4, 6, 6, 4, 61, 9, 6, 6, 6, 6, 5, 6, 4, 1, 6, 141, 110, 1356386766, 7, 66, 7, 6, 8, 4, 6, 5, 6, 6, 0, 5, 5, 4, 4, 1, 4, 141, 12, 3544495353, 7, 74, 9, 4, 2, 7, 8, 6, 4, 4, 5, 61, 9, 6, 6, 5, 5, 6, 6, 5, 1, 5, 126, 7, 102, 2212785524, 8, 103, 2749574982, 9, 7, 8, 10, 111, 2431310173, 11, 7, 9, 2, 1, 10, 141, 12, 3544495353, 7, 74, 8, 5, 0, 7, 11, 4, 4, 4, 6, 61, 8, 6, 4, 6, 4, 4, 4, 6, 1, 5, 141, 112, 404263772, 7, 66, 7, 4, 8, 5, 6, 5, 6, 4, 1, 4, 6, 5, 5, 1, 5, 141, 12, 3544495353, 7, 74, 9, 6, 2, 7, 8, 6, 4, 6, 5, 61, 9, 5, 6, 5, 6, 6, 4, 6, 1, 4, 68, 43828, 6, 6, 4, 0, 7, 1, 141, 12, 3544495353, 8, 74, 9, 6, 1, 8, 7, 5, 4, 4, 5, 61, 9, 5, 5, 6, 5, 6, 6, 6, 1, 5, 141, 113, 461828201, 7, 75, 4, 5, 6, 8, 1, 7, 6, 6, 5, 4, 5, 4, 141, 12, 3544495353, 7, 74, 9, 6, 1, 7, 8, 6, 6, 5, 5, 61, 9, 4, 6, 6, 4, 4, 6, 5, 1, 6, 141, 67, 1654912902, 7, 49, 8, 1, 7, 1, 142, 8, 122, 7, 7, 0, 7, 127, 4, 114, 3112891580, 6, 115, 2010573043, 5, 116, 2161265949, 3, 126, 7, 102, 2212785524, 8, 103, 2749574982, 9, 7, 8, 10, 117, 3311310483, 11, 7, 9, 2, 1, 10, 141, 12, 3544495353, 7, 74, 8, 5, 1, 7, 11, 4, 4, 5, 4, 61, 8, 6, 5, 5, 6, 6, 6, 6, 1, 6, 68, 2615, 4, 4, 5, 0, 7, 1, 141, 12, 3544495353, 8, 74, 9, 6, 2, 8, 7, 6, 5, 6, 5, 61, 9, 5, 4, 6, 6, 4, 4, 6, 1, 5, 141, 118, 1102056108, 7, 66, 7, 6, 8, 4, 4, 5, 5, 4, 0, 4, 6, 5, 4, 1, 5, 141, 12, 3544495353, 7, 74, 9, 4, 2, 7, 8, 5, 5, 6, 5, 61, 9, 5, 5, 5, 5, 5, 5, 5, 1, 5, 141, 119, 468083207, 7, 74, 8, 5, 1, 7, 1, 5, 5, 5, 5, 75, 5, 6, 5, 7, 1, 8, 5, 4, 5, 5, 6, 5, 141, 12, 3544495353, 8, 74, 9, 5, 0, 8, 7, 6, 6, 6, 5, 61, 9, 4, 5, 5, 6, 5, 4, 5, 1, 5, 142, 1, 122, 7, 7, 0, 7]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 20; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + this[op](); + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var VMPrototype = VM.prototype; +VMPrototype[88] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_INT_9_371437445 (specialized) + var dst = 9; + regs[base + dst] = 371437445; +}; +VMPrototype[OP.CALL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } +}; +VMPrototype[81] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[104] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // JUMP_REG_4 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 4]; +}; +VMPrototype[61] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_MOVE_1_5_8_3_9_2_6_4_0_7 (order: [1,5,8,3,9,2,6,4,0,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[4]]; + // MOVE + var dst = _operands[0]; + regs[base + dst] = regs[base + _operands[1]]; + // MUL + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a * regs[base + _operands[4]]; + // UNARY_POS + var dst = _operands[5]; + regs[base + dst] = +regs[base + _operands[6]]; + // DIV + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a / regs[base + _operands[9]]; +}; +VMPrototype[OP.JUMP_IF_TRUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[89] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_INT_10_134 (specialized) + var dst = 10; + regs[base + dst] = 134; +}; +VMPrototype[OP.STORE_UPVALUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); +}; +VMPrototype[107] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // THROW_106 (specialized) + throw regs[base + 106]; +}; +VMPrototype[OP.BOR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; +}; +VMPrototype[118] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[66] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_SUB_8_5_6_14_1_12_10_2_4_3_11_0_9_7_13 (order: [8,5,6,14,1,12,10,2,4,3,11,0,9,7,13]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[11], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[13], _unsortedOperands[0], _unsortedOperands[12], _unsortedOperands[6], _unsortedOperands[10], _unsortedOperands[5], _unsortedOperands[14], _unsortedOperands[3]]; + // MUL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a * regs[base + _operands[2]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[3]; + var count = _operands[4]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[5]]; + regs[base + dst] = elems; + // SUB + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a - regs[base + _operands[8]]; + // SHL + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a << regs[base + _operands[11]]; + // LT + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a < regs[base + _operands[14]]; +}; +VMPrototype[OP.SHL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; +}; +VMPrototype[OP.MOD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; +}; +VMPrototype[OP.CALL_METHOD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[OP.NEW] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } +}; +VMPrototype[123] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // GET_PROP,LOAD_CONST (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[109] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // JUMP_REG_95 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 95]; +}; +VMPrototype[OP.PATCH] = function () { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } +}; +VMPrototype[143] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_LOAD_GLOBAL_2_1_0 (order: [2,1,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + var globalName = this._constant(_operands[1], _operands[2]); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[101] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_THIS_2 (specialized) + var dst = 2; + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[124] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[OP.DELETE_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; +}; +VMPrototype[OP.TYPEOF_SAFE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; +}; +VMPrototype[82] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST_13_0_2916332409 (specialized) + var dst = 13; + regs[base + dst] = this._constant(0, 2916332409); +}; +VMPrototype[OP.BUILD_ARRAY] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; +}; +VMPrototype[136] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_INT,LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[62] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_UNARY_NEG_11_7_0_2_10_6_9_3_5_12_4_1_8 (order: [11,7,0,2,10,6,9,3,5,12,4,1,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[11], _unsortedOperands[3], _unsortedOperands[7], _unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[5], _unsortedOperands[1], _unsortedOperands[12], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[9]]; + // UNARY_NEG + var dst = _operands[0]; + regs[base + dst] = -regs[base + _operands[1]]; + // UNARY_POS + var dst = _operands[2]; + regs[base + dst] = +regs[base + _operands[3]]; + // BAND + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a & regs[base + _operands[6]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[9]]); + // BAND + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a & regs[base + _operands[12]]; +}; +VMPrototype[OP.DEFINE_SETTER] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); +}; +VMPrototype[OP.MAKE_CLOSURE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + // unified prototype for new/instanceof + regs[base + dst] = shell; +}; +VMPrototype[OP.ADD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; +}; +VMPrototype[100] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_UPVALUE_14_0 (specialized) + var dst = 14; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); +}; +VMPrototype[74] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_USHR_6_2_1_8_7_3_4_0_5 (order: [6,2,1,8,7,3,4,0,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[4], _unsortedOperands[3]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[0]; + var count = _operands[1]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[2]]; + regs[base + dst] = elems; + // NEQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a !== regs[base + _operands[5]]; + // USHR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >>> regs[base + _operands[8]]; +}; +VMPrototype[139] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_THIS,LOAD_GLOBAL (macro) + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[84] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST_14_2_640411250 (specialized) + var dst = 14; + regs[base + dst] = this._constant(2, 640411250); +}; +VMPrototype[OP.IN] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; +}; +VMPrototype[125] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[OP.JUMP_REG] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; +}; +VMPrototype[OP.JUMP_IF_FALSE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[99] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // RETURN_106 (specialized) + var retVal = regs[base + 106]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[122] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_CONST,RETURN (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // RETURN + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[117] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[OP.UNARY_NEG] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; +}; +VMPrototype[93] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // SET_PROP_15_3_17 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 15]; + var key = regs[base + 3]; + var val = regs[base + 17]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[OP.RETURN] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[77] = function () { + var fp = this._f; + var regs = this._regs; + // JUMP_3547 (specialized) + regs[fp + SLOTS.PC] = 3547; +}; +VMPrototype[OP.USHR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; +}; +VMPrototype[OP.EXP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); +}; +VMPrototype[OP.LTE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; +}; +VMPrototype[69] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_EQ_10_5_9_4_8_2_1_7_0_6_3_11 (order: [10,5,9,4,8,2,1,7,0,6,3,11]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[9], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[11]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[2]]); + // SHR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a >> regs[base + _operands[5]]; + // ADD + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a + regs[base + _operands[8]]; + // EQ + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a === regs[base + _operands[11]]; +}; +VMPrototype[OP.LOAD_UPVALUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); +}; +VMPrototype[115] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[138] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // GET_PROP,LOAD_CONST,LOAD_INT (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[79] = function () { + var fp = this._f; + var regs = this._regs; + // JUMP_5935 (specialized) + regs[fp + SLOTS.PC] = 5935; +}; +VMPrototype[144] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_GET_PROP_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + // dst = regs[obj][regs[key]] + var dst = _operands[0]; + var obj = regs[base + _operands[1]]; + var key = regs[base + _operands[2]]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.DEFINE_GETTER] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); +}; +VMPrototype[90] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_INT_11_11 (specialized) + var dst = 11; + regs[base + dst] = 11; +}; +VMPrototype[OP.BAND] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; +}; +VMPrototype[103] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // SET_PROP_11_12_13 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 11]; + var key = regs[base + 12]; + var val = regs[base + 13]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[86] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[142] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[76] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_UNARY_BITNOT_8_10_11_4_5_12_3_9_1_2_0_6_7 (order: [8,10,11,4,5,12,3,9,1,2,0,6,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[11], _unsortedOperands[12], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[5]]; + // GT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a > regs[base + _operands[2]]; + // LOAD_INT + var dst = _operands[3]; + regs[base + dst] = _operands[4]; + // UNARY_BITNOT + var dst = _operands[5]; + regs[base + dst] = ~regs[base + _operands[6]]; + // DIV + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a / regs[base + _operands[9]]; + // MOD + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a % regs[base + _operands[12]]; +}; +VMPrototype[129] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[112] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // RETURN_4 (specialized) + var retVal = regs[base + 4]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[72] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_SHL_6_5_4_0_2_1_3 (order: [6,5,4,0,2,1,3]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[6], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0]]; + // SHL + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a << regs[base + _operands[2]]; + // UNARY_NOT + var dst = _operands[3]; + regs[base + dst] = !regs[base + _operands[4]]; + // UNARY_NEG + var dst = _operands[5]; + regs[base + dst] = -regs[base + _operands[6]]; +}; +VMPrototype[OP.DEBUGGER] = function () { + debugger; +}; +VMPrototype[87] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_GLOBAL_15_4_2744943248 (specialized) + var dst = 15; + var globalName = this._constant(4, 2744943248); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[OP.LOAD_INT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[OP.SET_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[111] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // GET_PROP_8_7_74 (specialized) + // dst = regs[obj][regs[key]] + var dst = 8; + var obj = regs[base + 7]; + var key = regs[base + 74]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.GTE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; +}; +VMPrototype[OP.FINALLY_SETUP] = function () { + var fp = this._f; + var regs = this._regs; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); +}; +VMPrototype[OP.BXOR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; +}; +VMPrototype[78] = function () { + var fp = this._f; + var regs = this._regs; + // JUMP_9449 (specialized) + regs[fp + SLOTS.PC] = 9449; +}; +VMPrototype[127] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_CONST,LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[OP.NEQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; +}; +VMPrototype[108] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // GET_PROP_95_95_98 (specialized) + // dst = regs[obj][regs[key]] + var dst = 95; + var obj = regs[base + 95]; + var key = regs[base + 98]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.THROW] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + throw regs[base + this._operand()]; +}; +VMPrototype[116] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[OP.VOID] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + this._operand(); // consumes argument (intended) + // consumes argument (intended) + regs[base + dst] = undefined; +}; +VMPrototype[133] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_THIS,LOAD_CONST (macro) + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[OP.EQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; +}; +VMPrototype[OP.MUL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; +}; +VMPrototype[95] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_THIS_19 (specialized) + var dst = 19; + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[OP.LOAD_THIS] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[119] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST,GET_PROP (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.SUB] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; +}; +VMPrototype[OP.DIV] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; +}; +VMPrototype[137] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST,GET_PROP,LOAD_CONST,LOAD_INT (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[OP.UNARY_NOT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; +}; +VMPrototype[83] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST_12_1_1266097762 (specialized) + var dst = 12; + regs[base + dst] = this._constant(1, 1266097762); +}; +VMPrototype[OP.JUMP] = function () { + var fp = this._f; + var regs = this._regs; + regs[fp + SLOTS.PC] = this._operand(); +}; +VMPrototype[110] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_THIS_15 (specialized) + var dst = 15; + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[64] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_UNARY_NOT_3_0_7_2_6_4_5_1 (order: [3,0,7,2,6,4,5,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[4], _unsortedOperands[2]]; + // UNARY_NOT + var dst = _operands[0]; + regs[base + dst] = !regs[base + _operands[1]]; + // NEQ + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a !== regs[base + _operands[4]]; + // NEQ + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a !== regs[base + _operands[7]]; +}; +VMPrototype[131] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_GLOBAL,LOAD_CONST,GET_PROP (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.TRY_END] = function () { + var fp = this._f; + var regs = this._regs; + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); +}; +VMPrototype[121] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[OP.GET_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.BUILD_OBJECT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; +}; +VMPrototype[OP.LOOSE_EQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; +}; +VMPrototype[132] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[130] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_CONST,LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[114] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // JUMP_REG_87 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 87]; +}; +VMPrototype[OP.UNARY_BITNOT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; +}; +VMPrototype[70] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_MUL_4_1_0_3_2_9_6_5_8_7 (order: [4,1,0,3,2,9,6,5,8,7]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[4], _unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[8], _unsortedOperands[5]]; + // EXP + // Math.pow instead of `**` + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = Math.pow(a, regs[base + _operands[2]]); + // MUL + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a * regs[base + _operands[5]]; + // UNARY_NEG + var dst = _operands[6]; + regs[base + dst] = -regs[base + _operands[7]]; + // TYPEOF + var dst = _operands[8]; + regs[base + dst] = typeof regs[base + _operands[9]]; +}; +VMPrototype[65] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // ANTI_UNARY_POS_1_7_2_3_6_0_4_5_8 (order: [1,7,2,3,6,0,4,5,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[5], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[3], _unsortedOperands[6], _unsortedOperands[7], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[8]]; + // BUILD_ARRAY + // dst, count, [elemReg...] + var dst = _operands[0]; + var count = _operands[1]; + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + _operands[2]]; + regs[base + dst] = elems; + // JUMP_IF_FALSE + var src = _operands[3]; + var target = _operands[4]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + // UNARY_POS + var dst = _operands[5]; + regs[base + dst] = +regs[base + _operands[6]]; + // JUMP_IF_FALSE + var src = _operands[7]; + var target = _operands[8]; + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[134] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // LOAD_CONST,LOAD_THIS (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_THIS + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[126] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST,CALL_METHOD (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[OP.UNARY_POS] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; +}; +VMPrototype[OP.STORE_GLOBAL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; +}; +VMPrototype[OP.GT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; +}; +VMPrototype[OP.TRY_SETUP] = function () { + var fp = this._f; + var regs = this._regs; + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); +}; +VMPrototype[OP.LOOSE_NEQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; +}; +VMPrototype[96] = function () { + var fp = this._f; + var regs = this._regs; + // FINALLY_SETUP_3059_4_5_3269 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 3059, + contReg: 4, + payloadReg: 5, + throwPad: 3269 + }); +}; +VMPrototype[140] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_LOAD_INT_1_0 (order: [1,0]) + let _unsortedOperands = [this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = _operands[1]; +}; +VMPrototype[135] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST,CALL_METHOD (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // CALL_METHOD + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[68] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_ADD_1_3_2_0_6_4_5 (order: [1,3,2,0,6,4,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[5], _unsortedOperands[6], _unsortedOperands[4]]; + // LOAD_INT + var dst = _operands[0]; + regs[base + dst] = _operands[1]; + // TYPEOF + var dst = _operands[2]; + regs[base + dst] = typeof regs[base + _operands[3]]; + // ADD + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a + regs[base + _operands[6]]; +}; +VMPrototype[105] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // THROW_5 (specialized) + throw regs[base + 5]; +}; +VMPrototype[OP.INSTANCEOF] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; +}; +VMPrototype[75] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_BXOR_1_8_4_9_10_11_7_6_2_0_3_5 (order: [1,8,4,9,10,11,7,6,2,0,3,5]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[9], _unsortedOperands[0], _unsortedOperands[8], _unsortedOperands[10], _unsortedOperands[2], _unsortedOperands[11], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[5]]; + // BXOR + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a ^ regs[base + _operands[2]]; + // BOR + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a | regs[base + _operands[5]]; + // USHR + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >>> regs[base + _operands[8]]; + // BXOR + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a ^ regs[base + _operands[11]]; +}; +VMPrototype[98] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_GLOBAL_7_38_1493789366 (specialized) + var dst = 7; + var globalName = this._constant(38, 1493789366); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[97] = function () { + var fp = this._f; + var regs = this._regs; + // TRY_SETUP_3179_3 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 3179, + exceptionReg: 3 + }); +}; +VMPrototype[OP.MOVE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; +}; +VMPrototype[113] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // JUMP_REG_64 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 64]; +}; +VMPrototype[OP.SHR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; +}; +VMPrototype[106] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // GET_PROP_106_3_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 106; + var obj = regs[base + 3]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.FOR_IN_SETUP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; +}; +VMPrototype[71] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_LT_5_6_7_3_4_2_1_0 (order: [5,6,7,3,4,2,1,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[5], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[0], _unsortedOperands[1], _unsortedOperands[2]]; + // SUB + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a - regs[base + _operands[2]]; + // MOVE + var dst = _operands[3]; + regs[base + dst] = regs[base + _operands[4]]; + // LT + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a < regs[base + _operands[7]]; +}; +VMPrototype[OP.FOR_IN_NEXT] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } +}; +VMPrototype[85] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_CONST_7_3_2241976814 (specialized) + var dst = 7; + regs[base + dst] = this._constant(3, 2241976814); +}; +VMPrototype[94] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // RETURN_15 (specialized) + var retVal = regs[base + 15]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[OP.LT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; +}; +VMPrototype[OP.LOAD_CONST] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[120] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_INT,LOAD_INT,LOAD_INT (macro) + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); + // LOAD_INT + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[OP.LOAD_GLOBAL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[102] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_GLOBAL_11_4_2744943248 (specialized) + var dst = 11; + var globalName = this._constant(4, 2744943248); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[141] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ALIAS_LOAD_CONST_1_2_0 (order: [1,2,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[0], _unsortedOperands[1]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); +}; +VMPrototype[63] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_NEQ_6_2_8_1_10_13_5_3_11_4_0_14_9_7_12 (order: [6,2,8,1,10,13,5,3,11,4,0,14,9,7,12]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[9], _unsortedOperands[6], _unsortedOperands[0], _unsortedOperands[13], _unsortedOperands[2], _unsortedOperands[12], _unsortedOperands[4], _unsortedOperands[8], _unsortedOperands[14], _unsortedOperands[5], _unsortedOperands[11]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // EQ + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a === regs[base + _operands[5]]; + // GTE + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a >= regs[base + _operands[8]]; + // MUL + var dst = _operands[9]; + var a = regs[base + _operands[10]]; + regs[base + dst] = a * regs[base + _operands[11]]; + // SUB + var dst = _operands[12]; + var a = regs[base + _operands[13]]; + regs[base + dst] = a - regs[base + _operands[14]]; +}; +VMPrototype[92] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // GET_PROP_19_17_4 (specialized) + // dst = regs[obj][regs[key]] + var dst = 19; + var obj = regs[base + 17]; + var key = regs[base + 4]; + regs[base + dst] = obj[key]; +}; +VMPrototype[91] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_GLOBAL_17_4_2744943248 (specialized) + var dst = 17; + var globalName = this._constant(4, 2744943248); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[73] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_BOR_4_2_5_9_10_6_1_3_7_8_0 (order: [4,2,5,9,10,6,1,3,7,8,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[10], _unsortedOperands[6], _unsortedOperands[1], _unsortedOperands[7], _unsortedOperands[0], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[8], _unsortedOperands[9], _unsortedOperands[3], _unsortedOperands[4]]; + // UNARY_POS + var dst = _operands[0]; + regs[base + dst] = +regs[base + _operands[1]]; + // BOR + var dst = _operands[2]; + var a = regs[base + _operands[3]]; + regs[base + dst] = a | regs[base + _operands[4]]; + // GT + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a > regs[base + _operands[7]]; + // USHR + var dst = _operands[8]; + var a = regs[base + _operands[9]]; + regs[base + dst] = a >>> regs[base + _operands[10]]; +}; +VMPrototype[67] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // ANTI_BAND_1_5_6_3_4_7_2_8_0 (order: [1,5,6,3,4,7,2,8,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[8], _unsortedOperands[0], _unsortedOperands[6], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[1], _unsortedOperands[2], _unsortedOperands[5], _unsortedOperands[7]]; + // GT + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a > regs[base + _operands[2]]; + // BAND + var dst = _operands[3]; + var a = regs[base + _operands[4]]; + regs[base + dst] = a & regs[base + _operands[5]]; + // LT + var dst = _operands[6]; + var a = regs[base + _operands[7]]; + regs[base + dst] = a < regs[base + _operands[8]]; +}; +VMPrototype[OP.TYPEOF] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; +}; +VMPrototype[80] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_INT_18_8 (specialized) + var dst = 18; + regs[base + dst] = 8; +}; +VMPrototype[128] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // LOAD_GLOBAL,LOAD_CONST,GET_PROP,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // GET_PROP + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/qualification.json new file mode 100644 index 00000000..969fa837 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/qualification.json @@ -0,0 +1,81 @@ +{ + "id": "exceptions--tenOptionAliasShuffleHandlerStack--131", + "case": "exceptions", + "profile": "tenOptionAliasShuffleHandlerStack", + "seed": 131, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": true, + "encodeBytecode": false, + "concealConstants": true, + "controlFlowFlattening": true, + "dispatcher": true, + "stringConcealing": true, + "macroOpcodes": true, + "specializedOpcodes": true, + "aliasedOpcodes": true, + "antiInstrumentation": true, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": true, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "emitted-representation-components", + "observed": true, + "components": { + "stringConcealing": true, + "controlFlowFlattening": true, + "dispatcher": true, + "concealConstants": true, + "antiInstrumentation": true, + "specializedOpcodes": true, + "macroOpcodes": true, + "aliasedOpcodes": true, + "shuffleOpcodes": true, + "handlerTable": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/handler-table-alias-stack-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/encoded.js new file mode 100644 index 00000000..ce4e73c8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/encoded.js @@ -0,0 +1,17 @@ +var c={};function p(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var w=new WeakMap;function la(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ma(a){return a.C?a.F:a.g[a.B]}function na(a,g){a.C?a.F=g:a.g[a.B]=g}function C(a){this.U=a;this.o=[];this.prototype={}}function D(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function E(a){return a.j[a.g[a.h+0]++]} +function F(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,x=8+f.v,d=n+x;k.length>>13)&65535);return g}function T(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var x=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=x);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(x){case 0:var e=E(a);b[d+e]=G(a);break;case 1:e=E(a);b[d+e]=E(a);break;case 2:e=E(a);var M=G(a);if(!(M in a.i))throw new ReferenceError(`${M} is not defined`);b[d+e]=a.i[M];break; +case 3:e=E(a);b[d+e]=ma(b[f+4].o[E(a)]);break;case 4:e=E(a);b[d+e]=b[f+3];break;case 5:e=E(a);b[d+e]=b[d+E(a)];break;case 6:a.i[G(a)]=b[d+E(a)];break;case 7:var pa=E(a);na(b[f+4].o[pa],b[d+E(a)]);break;case 8:e=E(a);var q=b[d+E(a)],r=b[d+E(a)];b[d+e]=q[r];break;case 9:q=b[d+E(a)];r=b[d+E(a)];var z=b[d+E(a)];Reflect.set(q,r,z);break;case 10:e=E(a);q=b[d+E(a)];r=b[d+E(a)];b[d+e]=delete q[r];break;case 11:e=E(a);var h=b[d+E(a)];b[d+e]=h+b[d+E(a)];break;case 12:e=E(a);h=b[d+E(a)];b[d+e]=h-b[d+E(a)];break; +case 13:e=E(a);h=b[d+E(a)];b[d+e]=h*b[d+E(a)];break;case 14:e=E(a);h=b[d+E(a)];b[d+e]=h/b[d+E(a)];break;case 15:e=E(a);h=b[d+E(a)];b[d+e]=h%b[d+E(a)];break;case 60:e=E(a);h=b[d+E(a)];b[d+e]=Math.pow(h,b[d+E(a)]);break;case 16:e=E(a);h=b[d+E(a)];b[d+e]=h&b[d+E(a)];break;case 17:e=E(a);h=b[d+E(a)];b[d+e]=h|b[d+E(a)];break;case 18:e=E(a);h=b[d+E(a)];b[d+e]=h^b[d+E(a)];break;case 19:e=E(a);h=b[d+E(a)];b[d+e]=h<>b[d+E(a)];break;case 21:e=E(a);h=b[d+ +E(a)];b[d+e]=h>>>b[d+E(a)];break;case 22:e=E(a);h=b[d+E(a)];b[d+e]=hb[d+E(a)];break;case 24:e=E(a);h=b[d+E(a)];b[d+e]=h<=b[d+E(a)];break;case 25:e=E(a);h=b[d+E(a)];b[d+e]=h>=b[d+E(a)];break;case 26:e=E(a);h=b[d+E(a)];b[d+e]=h===b[d+E(a)];break;case 27:e=E(a);h=b[d+E(a)];b[d+e]=h!==b[d+E(a)];break;case 28:e=E(a);h=b[d+E(a)];b[d+e]=h==b[d+E(a)];break;case 29:e=E(a);h=b[d+E(a)];b[d+e]=h!=b[d+E(a)];break;case 30:e=E(a);h=b[d+E(a)];b[d+e]=h in b[d+E(a)]; +break;case 31:e=E(a);q=b[d+E(a)];b[d+e]=q instanceof b[d+E(a)];break;case 32:e=E(a);b[d+e]=-b[d+E(a)];break;case 33:e=E(a);b[d+e]=+b[d+E(a)];break;case 34:e=E(a);b[d+e]=!b[d+E(a)];break;case 35:e=E(a);b[d+e]=~b[d+E(a)];break;case 36:e=E(a);b[d+e]=typeof b[d+E(a)];break;case 37:e=E(a);E(a);b[d+e]=void 0;break;case 38:e=E(a);var Z=G(a);z=Object.prototype.hasOwnProperty.call(a.i,Z)?a.i[Z]:void 0;b[d+e]=typeof z;break;case 39:b[f+0]=E(a);break;case 40:var N=E(a),O=E(a);b[d+N]||(b[f+0]=O);break;case 41:N= +E(a);O=E(a);b[d+N]&&(b[f+0]=O);break;case 42:e=E(a);var t=b[d+E(a)],u=E(a);if(u===65535)l=b[d+E(a)];else for(l=Array(u),m=0;m>1):0]=H;break;case 46:throw b[d+E(a)];case 47:e=E(a);var ra=E(a),sa=E(a),ta=E(a),ca=E(a),ua=E(a),Q=Array(ca);for(m=0;m=K.J.length?b[f+0]=Aa:b[d+e]=K.J[K.M++];break;case 54:var y=b[f+5];y||(b[f+5]=y=[]);y.push({V:E(a),L:E(a)});break;case 55:b[f+5].pop();break;case 59:(y=b[f+5])||(b[f+5]=y=[]);y.push({T:E(a),S:E(a),Y:E(a),Z:E(a)});break;case 56:var ja=E(a),ka=E(a),Ba=E(a),L=E(a)^ja|0;for(f=ka;f>>13)>>>0;break;case 58:b[f+0]=b[d+E(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+x+" at pc "+(b[f+0]-1));}}catch(A){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}T(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw A;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=A,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=A,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var V=globalThis; +typeof window!=="undefined"&&(V.window=window,V.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(V.module=module,V.exports=typeof exports!=="undefined"?exports:void 0); +for(var Ca=U,W=p("BAAAAAEAAAAvAAAAAwAAAEUAAAABAAAACQAAAAAAAAAAAAAABQAAAAIAAAADAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAAEAAAAAQAAAAAAAAAAAAAABQAAAAIAAAAAAAAAIAAAAAYAAAAFAAAAKgAAAAUAAAACAAAAAQAAAAYAAAAAAAAABgAAAAMAAAAAAAAAKgAAAAcAAAACAAAAAQAAAAYAAAACAAAABgAAAAAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAgAAAAJAAAABgAAAAgAAAAwAAAABgAAAAMAAAAFAAAABwAAAAkAAAAJAAAAAwAAAAQAAAAGAAAAAAAAAAMAAAAFAAAAAAAAAC0AAAADAAAABAAAAAIAAAA7AAAAlQAAAAQAAAAFAAAApwAAADYAAAB+AAAAAwAAAAAAAAAGAAAABgAAAAAAAAAWAAAABwAAAAAAAAAGAAAAKAAAAAcAAABpAAAAAgAAAAYAAAAHAAAAAAAAAAAAAAAHAAAACAAAAAAAAAAsAAAACAAAAAYAAAABAAAABwAAAC4AAAAIAAAAAAAAAAYAAAACAAAAAAAAAAsAAAAHAAAAAAAAAAYAAAA3AAAABQAAAAUAAAAHAAAAAQAAAAQAAACpAAAANwAAACcAAACVAAAANwAAACcAAACPAAAAAAAAAAYAAAAJAAAAAAAAAAgAAAAHAAAAAwAAAAYAAAAFAAAABQAAAAcAAAABAAAABAAAAKsAAAA3AAAAJwAAAJUAAAA3AAAAAQAAAAQAAACtAAAAJwAAAJUAAAACAAAABgAAAAAAAAAAAAAAAAAAAAcAAAAEAAAAAAAAAAAAAAAIAAAACgAAAAAAAAAJAAAABgAAAAcAAAAIAAAAOgAAAAQAAAAuAAAABQAAAC0AAAAFAAAALQAAAAUAAAAAAAAABgAAAAUAAAAAAAAALQAAAAYAAAA="),X= +new Uint32Array(W.length/4),Y=0;Y>>0;Ca(new D(X,["window","TEST_OUTPUT",1,4,"finished",void 0,0,"Error","negative","message",!0],V),new C({m:0,v:10,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/qualification.json new file mode 100644 index 00000000..d925f2f2 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/qualification.json @@ -0,0 +1,73 @@ +{ + "id": "exceptions--minifyEncodeBytecode--173", + "case": "exceptions", + "profile": "minifyEncodeBytecode", + "seed": 173, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": true, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "minify": true, + "encodeBytecode": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-encoded-bytecode-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/encoded.js new file mode 100644 index 00000000..32a153d5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/encoded.js @@ -0,0 +1,24 @@ +var d={};function g(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(b){return b.charCodeAt(0)})}var m=new WeakMap;function r(a,b){this.g=a;this.B=b;this.C=!1;this.F=void 0}function t(a){this.U=a;this.o=[];this.prototype={}}function u(a,b,c){this.j=a;this.K=b;this.i=c;this.D=null;this.g=[];this.l=1;this.h=0}function v(a){return a.j[a.g[a.h+0]++]} +function x(a,b,c,e,f){for(var h=b.U,k=a.g,l=a.l,q=8+h.v,p=l+q;k.length>>13)&65535);return b}function A(a,b){var c=a.D;if(c){var e=a.g;a=b+e[b+6];for(b=e[b+7];b=e.length)break;b[c+0]=f+1;var h=a.j[f];typeof d.u==="number"&&(b[c+d.u]=(b[c+d.u]+1)%e.length);typeof d.A==="number"&&b[c+d.A]++;typeof d.P==="number"&&(b[c+d.P]=h);typeof d.H==="number"&&(b[c+d.H]=b[c+d.H]^f|0);try{a[h]()}catch(k){c=0;for(e=a.h;e;){if((f=b[e+5])&&f.length>0){c=e;break}A(a,e);a.l=e;e=b[e+1];a.h=e}if(!c)throw k;e=b[c+5].pop();f=b[c+7];e.L!==void 0?(b[f+e.L]=k,b[c+0]=e.V):(b[f+e.S]=e.Z,b[f+e.Y]=k,b[c+0]=e.T); +a.l=c+b[c+6];a.h=c}if(!a.h)return a.g[0]}}var C=u.prototype;C[0]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=z(this)};C[1]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=v(this)};C[2]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=z(this);if(!(e in this.i))throw new ReferenceError(`${e} is not defined`);a[b+c]=this.i[e]};C[3]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);a=b[a+4].o[v(this)];b[c+e]=a.C?a.F:a.g[a.B]}; +C[4]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);b[c+e]=b[a+3]};C[5]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=a[b+v(this)]};C[6]=function(){var a=this.g,b=a[this.h+7];this.i[z(this)]=a[b+v(this)]};C[7]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this);a=b[a+4].o[e];b=b[c+v(this)];a.C?a.F=b:a.g[a.B]=b};C[8]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)],f=a[b+v(this)];a[b+c]=e[f]}; +C[9]=function(){var a=this.g,b=a[this.h+7],c=a[b+v(this)],e=a[b+v(this)];a=a[b+v(this)];Reflect.set(c,e,a)};C[10]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)],f=a[b+v(this)];a[b+c]=delete e[f]};C[11]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e+a[b+v(this)]};C[12]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e-a[b+v(this)]};C[13]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e*a[b+v(this)]}; +C[14]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e/a[b+v(this)]};C[15]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e%a[b+v(this)]};C[60]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=Math.pow(e,a[b+v(this)])};C[16]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e&a[b+v(this)]};C[17]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e|a[b+v(this)]}; +C[18]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e^a[b+v(this)]};C[19]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e<>a[b+v(this)]};C[21]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e>>>a[b+v(this)]};C[22]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=ea[b+v(this)]};C[24]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e<=a[b+v(this)]};C[25]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e>=a[b+v(this)]};C[26]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e===a[b+v(this)]};C[27]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e!==a[b+v(this)]}; +C[28]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e==a[b+v(this)]};C[29]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e!=a[b+v(this)]};C[30]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e in a[b+v(this)]};C[31]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)];a[b+c]=e instanceof a[b+v(this)]};C[32]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=-a[b+v(this)]}; +C[33]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=+a[b+v(this)]};C[34]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=!a[b+v(this)]};C[35]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=~a[b+v(this)]};C[36]=function(){var a=this.g,b=a[this.h+7],c=v(this);a[b+c]=typeof a[b+v(this)]};C[37]=function(){var a=this.g,b=a[this.h+7],c=v(this);v(this);a[b+c]=void 0}; +C[38]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=z(this);e=Object.prototype.hasOwnProperty.call(this.i,e)?this.i[e]:void 0;a[b+c]=typeof e};C[39]=function(){this.g[this.h+0]=v(this)};C[40]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this),f=v(this);b[c+e]||(b[a+0]=f)};C[41]=function(){var a=this.h,b=this.g,c=b[a+7],e=v(this),f=v(this);b[c+e]&&(b[a+0]=f)}; +C[42]=function(){var a=this.g,b=a[this.h+7],c=v(this),e=a[b+v(this)],f=v(this);if(f===65535)var h=a[b+v(this)];else{h=Array(f);for(var k=0;k>1):0]=c}; +C[46]=function(){var a=this.g;throw a[a[this.h+7]+v(this)];}; +C[47]=function(){for(var a=this.h,b=this.g,c=b[a+7],e=v(this),f=v(this),h=v(this),k=v(this),l=v(this),q=v(this),p=Array(l),n=0;n=f.J.length?b[a+0]=h:b[c+e]=f.J[f.M++]}; +C[54]=function(){var a=this.h,b=this.g,c=b[a+5];c||(b[a+5]=c=[]);c.push({V:v(this),L:v(this)})};C[55]=function(){this.g[this.h+5].pop()};C[59]=function(){var a=this.h,b=this.g,c=b[a+5];c||(b[a+5]=c=[]);c.push({T:v(this),S:v(this),Y:v(this),Z:v(this)})};C[56]=function(){for(var a=v(this),b=v(this),c=v(this),e=v(this)^a|0,f=b;f>>13)>>>0};C[58]=function(){var a=this.h,b=this.g;b[a+0]=b[b[a+7]+v(this)]};C[57]=function(){debugger};var D=globalThis; +typeof window!=="undefined"&&(D.window=window,D.document=typeof document!=="undefined"?document:void 0);typeof module!=="undefined"&&(D.module=module,D.exports=typeof exports!=="undefined"?exports:void 0); +B(new u([4,1,47,3,69,1,9,0,0,5,2,3,2,3,0,0,0,4,1,0,0,5,2,0,32,6,5,42,5,2,1,6,0,6,3,0,42,7,2,1,6,2,6,0,0,0,8,4,0,8,9,6,8,48,6,3,5,7,9,9,3,4,6,0,3,5,0,45,3,4,2,59,149,4,5,167,54,126,3,0,6,6,0,22,7,0,6,40,7,105,2,6,7,0,0,7,8,0,44,8,6,1,7,46,8,0,6,2,0,11,7,0,6,55,5,5,7,1,4,169,55,39,149,55,39,143,0,6,9,0,8,7,3,6,5,5,7,1,4,171,55,39,149,55,1,4,173,39,149,2,6,0,0,0,7,4,0,0,8,10,0,9,6,7,8,58,4,46,5,45,5,45,5,0,6,5,0,45,6],["window","TEST_OUTPUT",1,4,"finished",void 0,0,"Error","negative","message",!0],D), +new t({m:0,v:10,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/qualification.json new file mode 100644 index 00000000..81aa557a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/qualification.json @@ -0,0 +1,73 @@ +{ + "id": "exceptions--minifyHandlerTable--178", + "case": "exceptions", + "profile": "minifyHandlerTable", + "seed": 178, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": true, + "minify": true, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "emitted-representation-components", + "observed": true, + "components": { + "minify": true, + "handlerTable": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-handler-table-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/encoded.js new file mode 100644 index 00000000..bdfe49e8 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/encoded.js @@ -0,0 +1,15 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0);T(new C([4,1,2,2,0,0,0,3,1,0,0,4,2,0,0,5,3,0,13,6,4,5,0,4,4,0,11,5,6,4,0,4,5,0,15,6,5,4,9,2,3,6,0,2,6,0,45,2],["window","TEST_OUTPUT",6,7,3,5,void 0],U),new B({m:0,v:7,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/qualification.json b/test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/qualification.json new file mode 100644 index 00000000..0f69087e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/qualification.json @@ -0,0 +1,57 @@ +{ + "id": "arithmetic--minify--1", + "case": "arithmetic", + "profile": "minify", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": true, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "container-declined", + "message": "extractContainerFromSource declined the input container" + }, + "sourceObservation": { + "kind": "value", + "value": 0 + }, + "encodedObservation": { + "kind": "value", + "value": 0 + }, + "decodedObservation": null, + "effectObservation": { + "axis": "runtime-minification", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/source.js b/test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/source.js new file mode 100644 index 00000000..f2632c75 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-boot/source.js @@ -0,0 +1 @@ +window.TEST_OUTPUT = (6 * 7 + 3) % 5; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/encoded.js new file mode 100644 index 00000000..4f42b732 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/encoded.js @@ -0,0 +1,15 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0);T(new C([4,1,47,4,55,1,5,0,0,5,2,4,0,4,0,0,42,5,2,1,4,5,3,5,2,4,1,0,0,5,2,0,42,6,3,0,42,7,3,0,48,8,2,6,7,9,4,5,8,0,4,3,0,45,4,4,2,5,3,0,47,4,77,0,3,1,0,1,3,45,4,0,4,3,0,45,4,3,0,0,0,1,4,0,11,2,0,1,7,0,2,45,2],[3,"window","TEST_OUTPUT",void 0,1],U),new B({m:0,v:9,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/qualification.json b/test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/qualification.json new file mode 100644 index 00000000..5e6be078 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/qualification.json @@ -0,0 +1,63 @@ +{ + "id": "closures--minify--1", + "case": "closures", + "profile": "minify", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": true, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "container-declined", + "message": "extractContainerFromSource declined the input container" + }, + "sourceObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 4, + 5 + ] + }, + "decodedObservation": null, + "effectObservation": { + "axis": "runtime-minification", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/source.js b/test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/source.js new file mode 100644 index 00000000..8148a2c5 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-closures/source.js @@ -0,0 +1 @@ +function counter(start) { let value = start; return () => ++value; } const next = counter(3); window.TEST_OUTPUT = [next(), next()]; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/encoded.js new file mode 100644 index 00000000..9256c163 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/encoded.js @@ -0,0 +1,17 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0); +T(new C([4,1,47,3,69,1,9,0,0,5,2,3,2,3,0,0,0,4,1,0,0,5,2,0,32,6,5,42,5,2,1,6,0,6,3,0,42,7,2,1,6,2,6,0,0,0,8,4,0,8,9,6,8,48,6,3,5,7,9,9,3,4,6,0,3,5,0,45,3,4,2,59,149,4,5,167,54,126,3,0,6,6,0,22,7,0,6,40,7,105,2,6,7,0,0,7,8,0,44,8,6,1,7,46,8,0,6,2,0,11,7,0,6,55,5,5,7,1,4,169,55,39,149,55,39,143,0,6,9,0,8,7,3,6,5,5,7,1,4,171,55,39,149,55,1,4,173,39,149,2,6,0,0,0,7,4,0,0,8,10,0,9,6,7,8,58,4,46,5,45,5,45,5,0,6,5,0,45,6],["window","TEST_OUTPUT",1,4,"finished",void 0,0,"Error","negative","message",!0],U), +new B({m:0,v:10,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/qualification.json new file mode 100644 index 00000000..8635f61d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/qualification.json @@ -0,0 +1,65 @@ +{ + "id": "exceptions--minify--4", + "case": "exceptions", + "profile": "minify", + "seed": 4, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": true, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "container-declined", + "message": "extractContainerFromSource declined the input container" + }, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": null, + "effectObservation": { + "axis": "runtime-minification", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/encoded.js b/test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/encoded.js new file mode 100644 index 00000000..895f9d10 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/encoded.js @@ -0,0 +1,15 @@ +var c={};function ha(a){return typeof Buffer!=="undefined"?Buffer.from(a,"base64"):Uint8Array.from(atob(a),function(g){return g.charCodeAt(0)})}var p=new WeakMap;function ia(a,g){this.g=a;this.B=g;this.C=!1;this.F=void 0}function ja(a){return a.C?a.F:a.g[a.B]}function ka(a,g){a.C?a.F=g:a.g[a.B]=g}function B(a){this.U=a;this.o=[];this.prototype={}}function C(a,g,b){this.j=a;this.K=g;this.i=b;this.D=null;this.g=[];this.l=1;this.h=0}function D(a){return a.j[a.g[a.h+0]++]} +function E(a,g,b,l,m){for(var f=g.U,k=a.g,n=a.l,w=8+f.v,d=n+w;k.length>>13)&65535);return g}function S(a,g){var b=a.D;if(b){var l=a.g;a=g+l[g+6];for(g=l[g+7];g=k.length)break;b[f+0]=n+1;var w=a.j[n];typeof c.u==="number"&&(b[f+c.u]=(b[f+c.u]+1)%k.length);typeof c.A==="number"&&b[f+c.A]++;typeof c.P==="number"&&(b[f+c.P]=w);typeof c.H==="number"&&(b[f+c.H]=b[f+c.H]^n|0);try{var d=b[f+7];switch(w){case 0:var e=D(a);b[d+e]=R(a);break;case 1:e=D(a);b[d+e]=D(a);break;case 2:e=D(a);var K=R(a);if(!(K in a.i))throw new ReferenceError(`${K} is not defined`);b[d+e]=a.i[K];break; +case 3:e=D(a);b[d+e]=ja(b[f+4].o[D(a)]);break;case 4:e=D(a);b[d+e]=b[f+3];break;case 5:e=D(a);b[d+e]=b[d+D(a)];break;case 6:a.i[R(a)]=b[d+D(a)];break;case 7:var ma=D(a);ka(b[f+4].o[ma],b[d+D(a)]);break;case 8:e=D(a);var q=b[d+D(a)],r=b[d+D(a)];b[d+e]=q[r];break;case 9:q=b[d+D(a)];r=b[d+D(a)];var y=b[d+D(a)];Reflect.set(q,r,y);break;case 10:e=D(a);q=b[d+D(a)];r=b[d+D(a)];b[d+e]=delete q[r];break;case 11:e=D(a);var h=b[d+D(a)];b[d+e]=h+b[d+D(a)];break;case 12:e=D(a);h=b[d+D(a)];b[d+e]=h-b[d+D(a)];break; +case 13:e=D(a);h=b[d+D(a)];b[d+e]=h*b[d+D(a)];break;case 14:e=D(a);h=b[d+D(a)];b[d+e]=h/b[d+D(a)];break;case 15:e=D(a);h=b[d+D(a)];b[d+e]=h%b[d+D(a)];break;case 60:e=D(a);h=b[d+D(a)];b[d+e]=Math.pow(h,b[d+D(a)]);break;case 16:e=D(a);h=b[d+D(a)];b[d+e]=h&b[d+D(a)];break;case 17:e=D(a);h=b[d+D(a)];b[d+e]=h|b[d+D(a)];break;case 18:e=D(a);h=b[d+D(a)];b[d+e]=h^b[d+D(a)];break;case 19:e=D(a);h=b[d+D(a)];b[d+e]=h<>b[d+D(a)];break;case 21:e=D(a);h=b[d+ +D(a)];b[d+e]=h>>>b[d+D(a)];break;case 22:e=D(a);h=b[d+D(a)];b[d+e]=hb[d+D(a)];break;case 24:e=D(a);h=b[d+D(a)];b[d+e]=h<=b[d+D(a)];break;case 25:e=D(a);h=b[d+D(a)];b[d+e]=h>=b[d+D(a)];break;case 26:e=D(a);h=b[d+D(a)];b[d+e]=h===b[d+D(a)];break;case 27:e=D(a);h=b[d+D(a)];b[d+e]=h!==b[d+D(a)];break;case 28:e=D(a);h=b[d+D(a)];b[d+e]=h==b[d+D(a)];break;case 29:e=D(a);h=b[d+D(a)];b[d+e]=h!=b[d+D(a)];break;case 30:e=D(a);h=b[d+D(a)];b[d+e]=h in b[d+D(a)]; +break;case 31:e=D(a);q=b[d+D(a)];b[d+e]=q instanceof b[d+D(a)];break;case 32:e=D(a);b[d+e]=-b[d+D(a)];break;case 33:e=D(a);b[d+e]=+b[d+D(a)];break;case 34:e=D(a);b[d+e]=!b[d+D(a)];break;case 35:e=D(a);b[d+e]=~b[d+D(a)];break;case 36:e=D(a);b[d+e]=typeof b[d+D(a)];break;case 37:e=D(a);D(a);b[d+e]=void 0;break;case 38:e=D(a);var V=R(a);y=Object.prototype.hasOwnProperty.call(a.i,V)?a.i[V]:void 0;b[d+e]=typeof y;break;case 39:b[f+0]=D(a);break;case 40:var L=D(a),M=D(a);b[d+L]||(b[f+0]=M);break;case 41:L= +D(a);M=D(a);b[d+L]&&(b[f+0]=M);break;case 42:e=D(a);var t=b[d+D(a)],u=D(a);if(u===65535)l=b[d+D(a)];else for(l=Array(u),m=0;m>1):0]=F;break;case 46:throw b[d+D(a)];case 47:e=D(a);var oa=D(a),pa=D(a),qa=D(a),Y=D(a),ra=D(a),O=Array(Y);for(m=0;m=I.J.length?b[f+0]=xa:b[d+e]=I.J[I.M++];break;case 54:var x=b[f+5];x||(b[f+5]=x=[]);x.push({V:D(a),L:D(a)});break;case 55:b[f+5].pop();break;case 59:(x=b[f+5])||(b[f+5]=x=[]);x.push({T:D(a),S:D(a),Y:D(a),Z:D(a)});break;case 56:var ea=D(a),fa=D(a),ya=D(a),J=D(a)^ea|0;for(f=fa;f>> +13)>>>0;break;case 58:b[f+0]=b[d+D(a)];break;case 57:debugger;break;default:throw Error("Unknown opcode: "+w+" at pc "+(b[f+0]-1));}}catch(z){f=0;for(k=a.h;k;){if((n=b[k+5])&&n.length>0){f=k;break}S(a,k);a.l=k;k=b[k+1];a.h=k}if(!f)throw z;k=b[f+5].pop();n=b[f+7];k.L!==void 0?(b[n+k.L]=z,b[f+0]=k.V):(b[n+k.S]=k.Z,b[n+k.Y]=z,b[f+0]=k.T);a.l=f+b[f+6];a.h=f}if(!a.h)return a.g[0]}}var U=globalThis;typeof window!=="undefined"&&(U.window=window,U.document=typeof document!=="undefined"?document:void 0); +typeof module!=="undefined"&&(U.module=module,U.exports=typeof exports!=="undefined"?exports:void 0);T(new C([4,1,47,4,53,1,8,1,0,1,2,5,3,4,0,4,0,0,5,2,4,2,4,1,0,0,5,2,0,0,6,3,0,42,7,3,1,6,48,6,2,7,2,9,4,5,6,0,4,4,0,45,4,4,2,59,83,3,4,102,0,5,5,0,11,6,0,5,5,4,6,1,3,104,55,39,83,55,1,3,106,39,83,3,5,0,5,6,5,0,6,5,0,11,7,5,6,7,0,7,58,3,46,4,45,4,0,5,4,0,45,5],[0,"window","TEST_OUTPUT",4,void 0,1],U),new B({m:0,v:8,G:0}),void 0,null); diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/qualification.json b/test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/qualification.json new file mode 100644 index 00000000..53fc52f4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/qualification.json @@ -0,0 +1,63 @@ +{ + "id": "tryFinally--minify--1", + "case": "tryFinally", + "profile": "minify", + "seed": 1, + "options": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": true, + "target": "browser" + }, + "status": "declined", + "diagnostic": { + "code": "container-declined", + "message": "extractContainerFromSource declined the input container" + }, + "sourceObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + 5, + 1 + ] + }, + "decodedObservation": null, + "effectObservation": { + "axis": "runtime-minification", + "observed": true + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/source.js b/test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/source.js new file mode 100644 index 00000000..747d80bf --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/minified-runtime-finally/source.js @@ -0,0 +1 @@ +let count = 0; function check(value) { try { return value + 1; } finally { count++; } } window.TEST_OUTPUT = [check(4), count]; diff --git a/test/vm/jsconfuser-vm/fixtures/qualification-provenance.md b/test/vm/jsconfuser-vm/fixtures/qualification-provenance.md new file mode 100644 index 00000000..aa80b3b4 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/qualification-provenance.md @@ -0,0 +1,30 @@ +# Retained qualification records + +A `qualification*.json` record describes its adjacent source/encoded pair: declared option vector and +seed, bounded `TEST_OUTPUT` observations, the historical decoder outcome, and an option-effect +observation. The adjacent source and encoded files are the executable evidence. For suffixed +records, the encoded filename uses the same suffix; the common source file is shared. + +`provenance.generation` distinguishes recorded historical facts from unknown facts. `null` means +not retained: it must not be filled from a present checkout, filesystem timestamp, or current +generator recipe. A recorded encoder revision is a historical assertion; it does not prove +that a local build was derived from that revision. These records do not retain the generation +module bytes, dependency environment, decoder output bytes, or complete one-option-removed comparison +control sets. The seven-option macro exception cell has a retained counterpart for its anti +component; its remaining component controls are absent. `completeEffectControlsRetained: false` +describes that incomplete set, not absence of every possible counterpart. Options and seed alone cannot reproduce the exact encoded file. + +Git history identifies the source/encoded bytes; the records carry no hashes of them. Executable +tests can recheck how those bytes behave and the current decoder's behavior; they cannot recover +the missing generation history. A historical +`accepted` status is a bounded cell outcome, not qualification of every program, seed or option +combination. Recorded `effectObservation` values remain historical comparisons whose complete control sets +cannot be independently rerun from this fixture population. Compound observations are true only when +all components are true, false when any is false, and otherwise null for an unavailable comparison. +This distinction does not change the separate decoder outcome. + +The focused terminal-switch, protected-catch, direct-return-finally and caught-return-finally +oracles retain their declared encoder revision, seed, RNG and options separately from the +qualification records. Git history identifies the adjacent source/encoded pair. Missing date, +Node version, tool bytes and execution manifest remain null; byte identity does not authenticate +the historical generation environment. diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/encoded.js b/test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/encoded.js new file mode 100644 index 00000000..50415007 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/encoded.js @@ -0,0 +1,1021 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */1, /* 3 */4, /* 4 */"finished", /* 5 */undefined, /* 6 */0, /* 7 */"Error", /* 8 */"negative", /* 9 */"message", /* 10 */true]; +var BYTECODE = "RSkAAAEAAACO6QAAAwAAAEUAAAABAAAACQAAAAAAAAAAAAAAMi8AAAIAAAADAAAAoN0AAAMAAAAAAAAAAAAAAEteAAAEAAAAAQAAAAAAAABLXgAABQAAAAIAAAAAAAAA/7YAAAYAAAAFAAAAQ1AAAAUAAAACAAAAAQAAAAYAAABLXgAABgAAAAMAAAAAAAAAQ1AAAAcAAAACAAAAAQAAAAYAAACg3QAABgAAAAAAAAAAAAAAS14AAAgAAAAEAAAAAAAAANdvAAAJAAAABgAAAAgAAABu1AAABgAAAAMAAAAFAAAABwAAAAkAAABCBQAAAwAAAAQAAAAGAAAAS14AAAMAAAAFAAAAAAAAAG5CAAADAAAARSkAAAIAAADa4gAAlQAAAAQAAAAFAAAApwAAAJWEAAB+AAAAAwAAAEteAAAGAAAABgAAAAAAAADfBQAABwAAAAAAAAAGAAAA2OkAAAcAAABpAAAAoN0AAAYAAAAHAAAAAAAAAEteAAAHAAAACAAAAAAAAAAUWwAACAAAAAYAAAABAAAABwAAAGYaAAAIAAAAS14AAAYAAAACAAAAAAAAAMeQAAAHAAAAAAAAAAYAAADeVgAAMi8AAAUAAAAHAAAAVDMAAAQAAACpAAAA3lYAAM2JAACVAAAA3lYAAM2JAACPAAAAS14AAAYAAAAJAAAAAAAAANdvAAAHAAAAAwAAAAYAAAAyLwAABQAAAAcAAABUMwAABAAAAKsAAADeVgAAzYkAAJUAAADeVgAAVDMAAAQAAACtAAAAzYkAAJUAAACg3QAABgAAAAAAAAAAAAAAS14AAAcAAAAEAAAAAAAAAEteAAAIAAAACgAAAAAAAABCBQAABgAAAAcAAAAIAAAAJE4AAAQAAABmGgAABQAAAG5CAAAFAAAAbkIAAAUAAABLXgAABgAAAAUAAAAAAAAAbkIAAAYAAAA="; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 10; +var ENCODE_BYTECODE = true; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 24139, + LOAD_INT: 13140, + LOAD_GLOBAL: 56736, + LOAD_UPVALUE: 53195, + LOAD_THIS: 10565, + MOVE: 12082, + STORE_GLOBAL: 10834, + STORE_UPVALUE: 57028, + GET_PROP: 28631, + SET_PROP: 1346, + DELETE_PROP: 38403, + ADD: 37063, + SUB: 25196, + MUL: 60691, + DIV: 17462, + MOD: 2150, + EXP: 57346, + BAND: 57872, + BOR: 1594, + BXOR: 26413, + SHL: 36452, + SHR: 26971, + USHR: 8972, + LT: 1503, + GT: 14322, + LTE: 17903, + GTE: 17433, + EQ: 47094, + NEQ: 5828, + LOOSE_EQ: 34922, + LOOSE_NEQ: 49919, + IN: 48388, + INSTANCEOF: 63842, + UNARY_NEG: 46847, + UNARY_POS: 31957, + UNARY_NOT: 53934, + UNARY_BITNOT: 63895, + TYPEOF: 40149, + VOID: 36227, + TYPEOF_SAFE: 24294, + JUMP: 35277, + JUMP_IF_FALSE: 59864, + JUMP_IF_TRUE: 55284, + CALL: 20547, + CALL_METHOD: 28321, + NEW: 23316, + RETURN: 17006, + THROW: 6758, + MAKE_CLOSURE: 59790, + BUILD_ARRAY: 54382, + BUILD_OBJECT: 13109, + DEFINE_GETTER: 35813, + DEFINE_SETTER: 10170, + FOR_IN_SETUP: 62543, + FOR_IN_NEXT: 50158, + TRY_SETUP: 33941, + TRY_END: 22238, + PATCH: 50050, + DEBUGGER: 9425, + JUMP_REG: 20004, + FINALLY_SETUP: 58074 +}; +var SENTINELS = { + CALL_SPREAD: 1020735734 +}; +var SLOTS = { + REG_BASE: 9, + HANDLERS: 4, + FRAME_SIZE: 1, + CLOSURE: 10, + NOISE_MIRROR: 5, + RET_DST: 8, + THIS: 7, + CALLER: 6, + NOISE_END: 12, + PC: 11 +}; +var HEADER_SIZE = 13; +var FRAME_START = 5; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/qualification.json b/test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/qualification.json new file mode 100644 index 00000000..556a6adc --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/qualification.json @@ -0,0 +1,73 @@ +{ + "id": "exceptions--randomizeOpcodesEncodeBytecode--155", + "case": "exceptions", + "profile": "randomizeOpcodesEncodeBytecode", + "seed": 155, + "options": { + "randomizeOpcodes": true, + "shuffleOpcodes": false, + "encodeBytecode": true, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "status": "accepted", + "diagnostic": null, + "sourceObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "encodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "decodedObservation": { + "kind": "value", + "value": [ + "negative", + 5, + true + ] + }, + "effectObservation": { + "axis": "serialized-bytecode-components", + "observed": true, + "components": { + "randomizeOpcodes": true, + "encodeBytecode": true + } + }, + "provenance": { + "kind": "retained-cell-with-incomplete-generation-provenance", + "generation": { + "encoderCommit": null, + "decoderCommit": null, + "toolSha256": null, + "nodeVersion": null, + "rng": null, + "generatedAt": null, + "executionProvenance": null + }, + "decodedBytesRetained": false, + "completeEffectControlsRetained": false + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/source.js b/test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/source.js new file mode 100644 index 00000000..4c670eac --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/randomized-encoded-exceptions/source.js @@ -0,0 +1 @@ +function check(value) { try { if (value < 0) throw new Error("negative"); return value + 1; } catch (error) { return error.message; } finally { window.finished = true; } } window.TEST_OUTPUT = [check(-1), check(4), window.finished]; diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/baseline.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/baseline.js new file mode 100644 index 00000000..3aaf7f05 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/baseline.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"CLEANED", /* 3 */"SAVED", /* 4 */undefined, /* 5 */7, /* 6 */"pauseGate", /* 7 */1, /* 8 */true]; +var BYTECODE = [4, 1, 47, 4, 81, 0, 6, 0, 0, 5, 2, 4, 47, 4, 141, 0, 8, 1, 0, 1, 2, 5, 3, 4, 2, 4, 0, 0, 0, 5, 1, 0, 42, 6, 3, 0, 2, 7, 0, 0, 0, 8, 2, 0, 8, 9, 7, 8, 2, 7, 0, 0, 0, 8, 3, 0, 8, 10, 7, 8, 43, 8, 7, 10, 0, 48, 7, 3, 6, 9, 8, 9, 4, 5, 7, 0, 4, 4, 0, 45, 4, 4, 1, 0, 3, 5, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 3, 0, 47, 5, 136, 0, 1, 1, 0, 1, 2, 9, 3, 4, 5, 2, 3, 0, 0, 0, 4, 6, 0, 8, 5, 3, 4, 43, 4, 3, 5, 0, 45, 2, 0, 3, 4, 0, 45, 3, 3, 0, 0, 45, 0, 4, 1, 59, 193, 2, 3, 211, 54, 171, 4, 3, 5, 0, 42, 6, 5, 0, 55, 5, 3, 6, 1, 2, 213, 55, 39, 193, 55, 39, 187, 0, 5, 7, 0, 32, 6, 5, 5, 3, 6, 1, 2, 215, 55, 39, 193, 55, 1, 2, 217, 39, 193, 2, 5, 0, 0, 0, 6, 2, 0, 0, 7, 8, 0, 9, 5, 6, 7, 58, 2, 46, 3, 45, 3, 45, 3, 0, 5, 4, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/encoded.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/encoded.js new file mode 100644 index 00000000..cf64a1d7 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/encoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"CLEANED", /* 3 */"SAVED", /* 4 */undefined, /* 5 */7, /* 6 */"pauseGate", /* 7 */1, /* 8 */true]; +var BYTECODE = "BAAAAAEAAAAvAAAABAAAAFEAAAAAAAAABgAAAAAAAAAAAAAABQAAAAIAAAAEAAAALwAAAAQAAACNAAAAAAAAAAgAAAABAAAAAAAAAAEAAAACAAAABQAAAAMAAAAEAAAAAgAAAAQAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAqAAAABgAAAAMAAAAAAAAAAgAAAAcAAAAAAAAAAAAAAAAAAAAIAAAAAgAAAAAAAAAIAAAACQAAAAcAAAAIAAAAAgAAAAcAAAAAAAAAAAAAAAAAAAAIAAAAAwAAAAAAAAAIAAAACgAAAAcAAAAIAAAAKwAAAAgAAAAHAAAACgAAAAAAAAAwAAAABwAAAAMAAAAGAAAACQAAAAgAAAAJAAAABAAAAAUAAAAHAAAAAAAAAAQAAAAEAAAAAAAAAC0AAAAEAAAABAAAAAEAAAAAAAAAAwAAAAUAAAAAAAAABQAAAAIAAAADAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAAEAAAAAwAAAAAAAAAvAAAABQAAAIgAAAAAAAAAAQAAAAEAAAAAAAAAAQAAAAIAAAAJAAAAAwAAAAQAAAAFAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAAEAAAABgAAAAAAAAAIAAAABQAAAAMAAAAEAAAAKwAAAAQAAAADAAAABQAAAAAAAAAtAAAAAgAAAAAAAAADAAAABAAAAAAAAAAtAAAAAwAAAAMAAAAAAAAAAAAAAC0AAAAAAAAABAAAAAEAAAA7AAAAwQAAAAIAAAADAAAA0wAAADYAAACrAAAABAAAAAMAAAAFAAAAAAAAACoAAAAGAAAABQAAAAAAAAA3AAAABQAAAAMAAAAGAAAAAQAAAAIAAADVAAAANwAAACcAAADBAAAANwAAACcAAAC7AAAAAAAAAAUAAAAHAAAAAAAAACAAAAAGAAAABQAAAAUAAAADAAAABgAAAAEAAAACAAAA1wAAADcAAAAnAAAAwQAAADcAAAABAAAAAgAAANkAAAAnAAAAwQAAAAIAAAAFAAAAAAAAAAAAAAAAAAAABgAAAAIAAAAAAAAAAAAAAAcAAAAIAAAAAAAAAAkAAAAFAAAABgAAAAcAAAA6AAAAAgAAAC4AAAADAAAALQAAAAMAAAAtAAAAAwAAAAAAAAAFAAAABAAAAAAAAAAtAAAABQAAAA=="; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = true; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/finalizers.src.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/finalizers.src.js new file mode 100644 index 00000000..f2fa2189 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/finalizers.src.js @@ -0,0 +1 @@ +function work() { let value = 7; window.SAVED = () => value; window.pauseGate(); return value; } function parent() { try { try { return work(); } catch (error) { return -1; } } finally { window.CLEANED = true; } } window.TEST_OUTPUT = [parent(), window.CLEANED, window.SAVED()]; diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/generation.json b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/generation.json new file mode 100644 index 00000000..675e39a1 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/generation.json @@ -0,0 +1,124 @@ +{ + "encoderPin": "20c5b96bf57337c568579352758d7685358650de", + "rng": { + "algorithm": "mulberry32", + "seed": 231 + }, + "toolchain": { + "node": "v26.10.0", + "jdk": "27", + "closure": "20260216.0.0" + }, + "generator": "scripts/qualify-jsconfuser-vm/timing.mjs", + "cases": { + "finalizers": "finalizers.src.js" + }, + "profiles": { + "baseline": {}, + "timing": { + "timingChecks": 100 + }, + "patch": { + "aliasedOpcodes": true, + "antiInstrumentation": true, + "macroOpcodes": true, + "selfModifying": true, + "specializedOpcodes": true + }, + "timingPatch": { + "aliasedOpcodes": true, + "antiInstrumentation": true, + "macroOpcodes": true, + "selfModifying": true, + "specializedOpcodes": true, + "timingChecks": 100 + }, + "encoded": { + "encodeBytecode": true + }, + "timingEncoded": { + "encodeBytecode": true, + "timingChecks": 100 + }, + "table": { + "handlerTable": true + }, + "timingTable": { + "handlerTable": true, + "timingChecks": 100 + } + }, + "boundary": "Known-source numeric nonminified, base64 payload/table dispatch/full PATCH stack; active timing atomic decline, disabled current recovery; no global JavaScript equivalence", + "baselineOptions": { + "randomizeOpcodes": false, + "shuffleOpcodes": false, + "encodeBytecode": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "stringConcealing": false, + "macroOpcodes": false, + "specializedOpcodes": false, + "aliasedOpcodes": false, + "antiInstrumentation": false, + "selfModifying": false, + "timingChecks": false, + "classObfuscation": false, + "handlerTable": false, + "minify": false, + "target": "browser" + }, + "separateGeneration": { + "topLevelFinally": { + "worker": "scripts/qualify-jsconfuser-vm/worker.mjs", + "driver": "scripts/qualify-jsconfuser-vm/qualify.mjs", + "api": "runWorker(request, 65000)", + "sourceFile": "top-level-finally.src.js", + "outputFile": "top-level-finally.js", + "request": { + "action": "generate", + "seed": 231, + "options": { + "aliasedOpcodes": false, + "antiInstrumentation": false, + "classObfuscation": false, + "concealConstants": false, + "controlFlowFlattening": false, + "dispatcher": false, + "encodeBytecode": false, + "handlerTable": false, + "macroOpcodes": false, + "minify": false, + "randomizeOpcodes": false, + "selfModifying": false, + "shuffleOpcodes": false, + "specializedOpcodes": false, + "stringConcealing": false, + "target": "browser", + "timingChecks": false + } + }, + "rng": { + "algorithm": "mulberry32", + "seed": 231 + }, + "toolchain": { + "node": "v26.10.0", + "jdk": "27", + "closure": "20260216.0.0" + }, + "encoderPin": "20c5b96bf57337c568579352758d7685358650de", + "protocol": { + "journalPath": "Fresh output directory/generation.ndjson, initialized empty before runWorker", + "identities": "java and jar from audit.mjs snapshot({ generation: true })", + "source": "Read sourceFile as UTF-8; pass its exact string as request.source", + "rawNormalization": "Apply timing.mjs leading-line-comment removal to result.raw before writing outputFile", + "boundsMs": { + "worker": 65000, + "closure": 60000 + }, + "completion": "Require result.ok, exitCode 0, no signal or timeout, equal starting/final snapshot and exact output readback" + } + } + } +} diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/oracle.json b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/oracle.json new file mode 100644 index 00000000..73884006 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/oracle.json @@ -0,0 +1,13 @@ +{ + "normal": [ + 7, + true, + 7 + ], + "poisonErrorKinds": [ + "Error", + "TypeError" + ], + "activeThreshold": 100, + "realDelayMs": 250 +} diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/patch.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/patch.js new file mode 100644 index 00000000..6c3eb42e --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/patch.js @@ -0,0 +1,1547 @@ +var CONSTANTS = [/* 0 */"SonY", /* 1 */"lzWinvd", /* 2 */7, /* 3 */"window", /* 4 */"SAVED", /* 5 */"pauseGate", /* 6 */undefined, /* 7 */"83mWf", /* 8 */"Wfsc", /* 9 */49652, /* 10 */1, /* 11 */"CLEANED", /* 12 */true, /* 13 */50219, /* 14 */"oQMIV", /* 15 */"TEST_OUTPUT"]; +var BYTECODE = [56, 5, 471, 504, 972794029, 25, 23, 0, 15, 56, 387, 401, 405, 2594487475, 56, 395, 388, 403, 4255364308, 55, 11, 19, 22, 22, 34, 13, 8, 56, 441, 522, 538, 1046672767, 34, 0, 7, 34, 16, 18, 56, 43, 505, 521, 193231388, 13, 12, 11, 21, 56, 558, 538, 548, 1065703205, 56, 444, 386, 396, 2401672881, 55, 47, 56, 64, 274, 304, 2774722743, 17, 16, 23, 2, 1, 4, 4202, 13, 23, 20, 3, 16, 2, 18, 10, 4, 4, 56, 389, 456, 461, 3205376763, 5, 23, 11, 56, 567, 407, 410, 2124022955, 95, 3, 0, 0, 4, 1, 0, 77, 97, 0, 2, 5, 61, 5, 3, 4, 3, 3, 3, 3, 3, 3, 3, 45465, 3, 2, 78, 97, 0, 4, 6, 47, 7, 159, 0, 1, 1, 0, 1, 2, 79, 96, 5, 3, 0, 6, 5, 0, 80, 43, 6, 5, 7, 0, 81, 97, 0, 6, 5, 56, 158, 470, 471, 1055874927, 33, 56, 164, 467, 470, 797628711, 4, 13, 46, 95, 6, 7, 0, 7, 8, 0, 97, 0, 9, 5, 84, 85, 86, 87, 42, 9, 8, 0, 55, 61, 9, 5, 6, 6, 5, 6, 5, 5, 5, 5, 33512, 6, 3, 88, 55, 63, 55, 56, 210, 504, 505, 3518192882, 54, 97, 0, 10, 8, 62, 7, 5, 5, 7, 8, 9, 7, 7, 7, 5, 61, 9, 6, 6, 5, 7, 7, 7, 7, 6, 7, 63764, 6, 3, 90, 55, 56, 247, 273, 274, 3069764117, 2, 55, 91, 63, 96, 8, 3, 0, 9, 11, 0, 97, 0, 12, 10, 92, 93, 94, 64, 64, 97, 0, 6, 8, 98, 8, 1429402918, 1134067273, 3788952303, 2148357739, 507526621, 3162352144, 1521292916, 4175493751, 2535067139, 894933672, 3549115996, 1908846023, 268231245, 2922473142, 1282011746, 3936640988, 2296300122, 655410883, 3310312882, 1669590405, 28862113, 2682985048, 1042722799, 3696769643, 2056619767, 416090669, 3070295434, 1430132383, 4084441748, 2444181431, 803361776, 1, 14, 57082, 25, 19, 24, 7, 56, 543, 388, 403, 1473363333, 25, 9, 10, 7, 56, 419, 562, 564, 3830279906, 11, 9, 22, 2, 56, 530, 418, 419, 2959897142, 24, 20, 5, 15, 5, 24, 0, 56, 388, 348, 363, 884861677, 32, 24, 7, 11, 7, 4, 23, 34, 2, 0, 25, 14, 7, 7, 12, 16, 5, 18, 16, 8, 2, 18, 5, 10, 11, 55, 8, 16, 1, 18, 20, 10, 4, 1, 16, 11, 24, 22, 55, 4, 56, 539, 400, 408, 3652244719, 8, 5, 9, 4, 32, 0, 13, 35, 23, 13, 22, 23, 10, 2, 55, 34, 4, 5, 9, 21, 20, 18, 23, 34, 24, 5, 56, 533, 389, 403, 1459612355, 13, 13, 22, 16, 56, 548, 396, 404, 922356307, 24, 21, 14, 23, 34, 6, 9, 56, 542, 560, 561, 2821963211, 55, 36, 4, 15, 56, 391, 421, 437, 1653398605, 56, 432, 428, 441, 2070020070, 35, 8, 18, 32, 22, 2, 17, 18, 23, 4, 3452190077, 1811583833, 170989348, 3709874382, 3627410361, 1986604812, 346206336, 3000768879, 1360232907, 4014995684, 2373687112, 733359043, 3387473758, 1747265745, 106811721, 2760931832, 1120846191, 3775092961, 2134389669, 494120871, 3148832929, 1507915967, 4162744746, 2522192273, 881393813, 3536216894, 1895047166, 254765946, 2909003938, 1268537732, 3922866471, 2282272899, 642072757, 3296579464, 1655731092, 15390879, 2670108817, 1877584598, 2847850002, 1206997556, 3861763632, 2221294093, 580439582, 3235336706, 1594504708, 4249333240, 2608011778, 967767565, 3621784090, 1981673413, 341117493, 2995303999, 1355160174, 4009452130, 21, 19, 15, 19, 8, 23, 4, 3, 34, 3, 15, 56, 541, 392, 407, 560394189, 56, 527, 554, 561, 145549988, 16, 21, 3, 0, 34, 14, 3, 56, 410, 395, 408, 1351911488, 20, 11, 11, 12, 12, 0, 6, 10, 16, 21, 15, 24, 24, 21, 21, 24, 4, 16]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 13; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 63: + { + // JUMP_251 (specialized) + regs[fp + SLOTS.PC] = 251; + break; + } + case 64: + { + // RETURN_3 (specialized) + var retVal = regs[base + 3]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 65: + { + // LOAD_CONST_5_13_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(13, 0); + break; + } + case 66: + { + // LOAD_CONST_4_14_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(14, 0); + break; + } + case 67: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 68: + { + // LOAD_GLOBAL_6_3_0 (specialized) + var dst = 6; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 69: + { + // LOAD_CONST_7_15_0 (specialized) + var dst = 7; + regs[base + dst] = this._constant(15, 0); + break; + } + case 70: + { + // LOAD_GLOBAL_9_3_0 (specialized) + var dst = 9; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 71: + { + // LOAD_CONST_10_11_0 (specialized) + var dst = 10; + regs[base + dst] = this._constant(11, 0); + break; + } + case 72: + { + // GET_PROP_11_9_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 11; + var obj = regs[base + 9]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; + break; + } + case 73: + { + // LOAD_GLOBAL_9_3_0 (specialized) + var dst = 9; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 74: + { + // GET_PROP_12_9_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 12; + var obj = regs[base + 9]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; + break; + } + case 75: + { + // SET_PROP_6_7_9 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 6]; + var key = regs[base + 7]; + var val = regs[base + 9]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 76: + { + // RETURN_6 (specialized) + var retVal = regs[base + 6]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 77: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 78: + { + // LOAD_GLOBAL_5_3_0 (specialized) + var dst = 5; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 79: + { + // SET_PROP_5_6_7 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 5]; + var key = regs[base + 6]; + var val = regs[base + 7]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 80: + { + // GET_PROP_7_5_6 (specialized) + // dst = regs[obj][regs[key]] + var dst = 7; + var obj = regs[base + 5]; + var key = regs[base + 6]; + regs[base + dst] = obj[key]; + break; + } + case 81: + { + // RETURN_2 (specialized) + var retVal = regs[base + 2]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 82: + { + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 83: + { + // LOAD_UPVALUE_0_0 (specialized) + var dst = 0; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 84: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 85: + { + // FINALLY_SETUP_251_2_3_264 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 251, + contReg: 2, + payloadReg: 3, + throwPad: 264 + }); + break; + } + case 86: + { + // TRY_SETUP_211_4 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 211, + exceptionReg: 4 + }); + break; + } + case 87: + { + // LOAD_UPVALUE_8_0 (specialized) + var dst = 8; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 88: + { + // LOAD_INT_2_265 (specialized) + var dst = 2; + regs[base + dst] = 265; + break; + } + case 89: + { + // JUMP_248 (specialized) + regs[fp + SLOTS.PC] = 248; + break; + } + case 90: + { + // LOAD_INT_2_266 (specialized) + var dst = 2; + regs[base + dst] = 266; + break; + } + case 91: + { + // LOAD_INT_2_267 (specialized) + var dst = 2; + regs[base + dst] = 267; + break; + } + case 92: + { + // SET_PROP_8_9_10 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 8]; + var key = regs[base + 9]; + var val = regs[base + 10]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 93: + { + // JUMP_REG_2 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 2]; + break; + } + case 94: + { + // THROW_3 (specialized) + throw regs[base + 3]; + break; + } + case 95: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 96: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 97: + { + // ALIAS_LOAD_CONST_2_1_0 (order: [2,1,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 98: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 61: + { + // ANTI_MOVE_9_0_3_5_6_12_1_7_11_2_4_10_8 (order: [9,0,3,5,6,12,1,7,11,2,4,10,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[12], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[8], _unsortedOperands[5]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // LOAD_INT + var dst = _operands[3]; + regs[base + dst] = _operands[4]; + // LTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a <= regs[base + _operands[7]]; + // MOVE + var dst = _operands[8]; + regs[base + dst] = regs[base + _operands[9]]; + // BAND + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a & regs[base + _operands[12]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_9_8_4_0_3_2_6_5_7_1 (order: [9,8,4,0,3,2,6,5,7,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[0]]; + // UNARY_NOT + var dst = _operands[0]; + regs[base + dst] = !regs[base + _operands[1]]; + // UNARY_NEG + var dst = _operands[2]; + regs[base + dst] = -regs[base + _operands[3]]; + // ADD + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a + regs[base + _operands[6]]; + // BXOR + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a ^ regs[base + _operands[9]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/table.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/table.js new file mode 100644 index 00000000..dcf3c0c0 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/table.js @@ -0,0 +1,1028 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"CLEANED", /* 3 */"SAVED", /* 4 */undefined, /* 5 */7, /* 6 */"pauseGate", /* 7 */1, /* 8 */true]; +var BYTECODE = [4, 1, 47, 4, 81, 0, 6, 0, 0, 5, 2, 4, 47, 4, 141, 0, 8, 1, 0, 1, 2, 5, 3, 4, 2, 4, 0, 0, 0, 5, 1, 0, 42, 6, 3, 0, 2, 7, 0, 0, 0, 8, 2, 0, 8, 9, 7, 8, 2, 7, 0, 0, 0, 8, 3, 0, 8, 10, 7, 8, 43, 8, 7, 10, 0, 48, 7, 3, 6, 9, 8, 9, 4, 5, 7, 0, 4, 4, 0, 45, 4, 4, 1, 0, 3, 5, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 3, 0, 47, 5, 136, 0, 1, 1, 0, 1, 2, 9, 3, 4, 5, 2, 3, 0, 0, 0, 4, 6, 0, 8, 5, 3, 4, 43, 4, 3, 5, 0, 45, 2, 0, 3, 4, 0, 45, 3, 3, 0, 0, 45, 0, 4, 1, 59, 193, 2, 3, 211, 54, 171, 4, 3, 5, 0, 42, 6, 5, 0, 55, 5, 3, 6, 1, 2, 213, 55, 39, 193, 55, 39, 187, 0, 5, 7, 0, 32, 6, 5, 5, 3, 6, 1, 2, 215, 55, 39, 193, 55, 1, 2, 217, 39, 193, 2, 5, 0, 0, 0, 6, 2, 0, 0, 7, 8, 0, 9, 5, 6, 7, 58, 2, 46, 3, 45, 3, 45, 3, 0, 5, 4, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + this[op](); + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var VMPrototype = VM.prototype; +VMPrototype[OP.LOAD_CONST] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[OP.LOAD_INT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[OP.LOAD_GLOBAL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[OP.LOAD_UPVALUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); +}; +VMPrototype[OP.LOAD_THIS] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[OP.MOVE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; +}; +VMPrototype[OP.STORE_GLOBAL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; +}; +VMPrototype[OP.STORE_UPVALUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); +}; +VMPrototype[OP.GET_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.SET_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[OP.DELETE_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; +}; +VMPrototype[OP.ADD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; +}; +VMPrototype[OP.SUB] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; +}; +VMPrototype[OP.MUL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; +}; +VMPrototype[OP.DIV] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; +}; +VMPrototype[OP.MOD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; +}; +VMPrototype[OP.EXP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); +}; +VMPrototype[OP.BAND] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; +}; +VMPrototype[OP.BOR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; +}; +VMPrototype[OP.BXOR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; +}; +VMPrototype[OP.SHL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; +}; +VMPrototype[OP.SHR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; +}; +VMPrototype[OP.USHR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; +}; +VMPrototype[OP.LT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; +}; +VMPrototype[OP.GT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; +}; +VMPrototype[OP.LTE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; +}; +VMPrototype[OP.GTE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; +}; +VMPrototype[OP.EQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; +}; +VMPrototype[OP.NEQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; +}; +VMPrototype[OP.LOOSE_EQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; +}; +VMPrototype[OP.LOOSE_NEQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; +}; +VMPrototype[OP.IN] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; +}; +VMPrototype[OP.INSTANCEOF] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_NEG] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_POS] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_NOT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_BITNOT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; +}; +VMPrototype[OP.TYPEOF] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; +}; +VMPrototype[OP.VOID] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + this._operand(); // consumes argument (intended) + // consumes argument (intended) + regs[base + dst] = undefined; +}; +VMPrototype[OP.TYPEOF_SAFE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; +}; +VMPrototype[OP.JUMP] = function () { + var fp = this._f; + var regs = this._regs; + regs[fp + SLOTS.PC] = this._operand(); +}; +VMPrototype[OP.JUMP_IF_FALSE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[OP.JUMP_IF_TRUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[OP.CALL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } +}; +VMPrototype[OP.CALL_METHOD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[OP.NEW] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } +}; +VMPrototype[OP.RETURN] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[OP.THROW] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + throw regs[base + this._operand()]; +}; +VMPrototype[OP.MAKE_CLOSURE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + // unified prototype for new/instanceof + regs[base + dst] = shell; +}; +VMPrototype[OP.BUILD_ARRAY] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; +}; +VMPrototype[OP.BUILD_OBJECT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; +}; +VMPrototype[OP.DEFINE_GETTER] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); +}; +VMPrototype[OP.DEFINE_SETTER] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); +}; +VMPrototype[OP.FOR_IN_SETUP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; +}; +VMPrototype[OP.FOR_IN_NEXT] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } +}; +VMPrototype[OP.TRY_SETUP] = function () { + var fp = this._f; + var regs = this._regs; + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); +}; +VMPrototype[OP.TRY_END] = function () { + var fp = this._f; + var regs = this._regs; + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); +}; +VMPrototype[OP.FINALLY_SETUP] = function () { + var fp = this._f; + var regs = this._regs; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); +}; +VMPrototype[OP.PATCH] = function () { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } +}; +VMPrototype[OP.JUMP_REG] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; +}; +VMPrototype[OP.DEBUGGER] = function () { + debugger; +}; +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timing.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timing.js new file mode 100644 index 00000000..ba536f38 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timing.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"CLEANED", /* 3 */"SAVED", /* 4 */undefined, /* 5 */7, /* 6 */"pauseGate", /* 7 */1, /* 8 */true]; +var BYTECODE = [4, 1, 47, 4, 81, 0, 6, 0, 0, 5, 2, 4, 47, 4, 141, 0, 8, 1, 0, 1, 2, 5, 3, 4, 2, 4, 0, 0, 0, 5, 1, 0, 42, 6, 3, 0, 2, 7, 0, 0, 0, 8, 2, 0, 8, 9, 7, 8, 2, 7, 0, 0, 0, 8, 3, 0, 8, 10, 7, 8, 43, 8, 7, 10, 0, 48, 7, 3, 6, 9, 8, 9, 4, 5, 7, 0, 4, 4, 0, 45, 4, 4, 1, 0, 3, 5, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 3, 0, 47, 5, 136, 0, 1, 1, 0, 1, 2, 9, 3, 4, 5, 2, 3, 0, 0, 0, 4, 6, 0, 8, 5, 3, 4, 43, 4, 3, 5, 0, 45, 2, 0, 3, 4, 0, 45, 3, 3, 0, 0, 45, 0, 4, 1, 59, 193, 2, 3, 211, 54, 171, 4, 3, 5, 0, 42, 6, 5, 0, 55, 5, 3, 6, 1, 2, 213, 55, 39, 193, 55, 39, 187, 0, 5, 7, 0, 32, 6, 5, 5, 3, 6, 1, 2, 215, 55, 39, 193, 55, 1, 2, 217, 39, 193, 2, 5, 0, 0, 0, 6, 2, 0, 0, 7, 8, 0, 9, 5, 6, 7, 58, 2, 46, 3, 45, 3, 45, 3, 0, 5, 4, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = 100; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingEncoded.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingEncoded.js new file mode 100644 index 00000000..ff87d93a --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingEncoded.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"CLEANED", /* 3 */"SAVED", /* 4 */undefined, /* 5 */7, /* 6 */"pauseGate", /* 7 */1, /* 8 */true]; +var BYTECODE = "BAAAAAEAAAAvAAAABAAAAFEAAAAAAAAABgAAAAAAAAAAAAAABQAAAAIAAAAEAAAALwAAAAQAAACNAAAAAAAAAAgAAAABAAAAAAAAAAEAAAACAAAABQAAAAMAAAAEAAAAAgAAAAQAAAAAAAAAAAAAAAAAAAAFAAAAAQAAAAAAAAAqAAAABgAAAAMAAAAAAAAAAgAAAAcAAAAAAAAAAAAAAAAAAAAIAAAAAgAAAAAAAAAIAAAACQAAAAcAAAAIAAAAAgAAAAcAAAAAAAAAAAAAAAAAAAAIAAAAAwAAAAAAAAAIAAAACgAAAAcAAAAIAAAAKwAAAAgAAAAHAAAACgAAAAAAAAAwAAAABwAAAAMAAAAGAAAACQAAAAgAAAAJAAAABAAAAAUAAAAHAAAAAAAAAAQAAAAEAAAAAAAAAC0AAAAEAAAABAAAAAEAAAAAAAAAAwAAAAUAAAAAAAAABQAAAAIAAAADAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAAEAAAAAwAAAAAAAAAvAAAABQAAAIgAAAAAAAAAAQAAAAEAAAAAAAAAAQAAAAIAAAAJAAAAAwAAAAQAAAAFAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAAEAAAABgAAAAAAAAAIAAAABQAAAAMAAAAEAAAAKwAAAAQAAAADAAAABQAAAAAAAAAtAAAAAgAAAAAAAAADAAAABAAAAAAAAAAtAAAAAwAAAAMAAAAAAAAAAAAAAC0AAAAAAAAABAAAAAEAAAA7AAAAwQAAAAIAAAADAAAA0wAAADYAAACrAAAABAAAAAMAAAAFAAAAAAAAACoAAAAGAAAABQAAAAAAAAA3AAAABQAAAAMAAAAGAAAAAQAAAAIAAADVAAAANwAAACcAAADBAAAANwAAACcAAAC7AAAAAAAAAAUAAAAHAAAAAAAAACAAAAAGAAAABQAAAAUAAAADAAAABgAAAAEAAAACAAAA1wAAADcAAAAnAAAAwQAAADcAAAABAAAAAgAAANkAAAAnAAAAwQAAAAIAAAAFAAAAAAAAAAAAAAAAAAAABgAAAAIAAAAAAAAAAAAAAAcAAAAIAAAAAAAAAAkAAAAFAAAABgAAAAcAAAA6AAAAAgAAAC4AAAADAAAALQAAAAMAAAAtAAAAAwAAAAAAAAAFAAAABAAAAAAAAAAtAAAABQAAAA=="; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = true; +var TIMING_CHECKS = 100; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingPatch.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingPatch.js new file mode 100644 index 00000000..4ece0c9f --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingPatch.js @@ -0,0 +1,1547 @@ +var CONSTANTS = [/* 0 */"SonY", /* 1 */"lzWinvd", /* 2 */7, /* 3 */"window", /* 4 */"SAVED", /* 5 */"pauseGate", /* 6 */undefined, /* 7 */"83mWf", /* 8 */"Wfsc", /* 9 */49652, /* 10 */1, /* 11 */"CLEANED", /* 12 */true, /* 13 */50219, /* 14 */"oQMIV", /* 15 */"TEST_OUTPUT"]; +var BYTECODE = [56, 5, 471, 504, 972794029, 25, 23, 0, 15, 56, 387, 401, 405, 2594487475, 56, 395, 388, 403, 4255364308, 55, 11, 19, 22, 22, 34, 13, 8, 56, 441, 522, 538, 1046672767, 34, 0, 7, 34, 16, 18, 56, 43, 505, 521, 193231388, 13, 12, 11, 21, 56, 558, 538, 548, 1065703205, 56, 444, 386, 396, 2401672881, 55, 47, 56, 64, 274, 304, 2774722743, 17, 16, 23, 2, 1, 4, 4202, 13, 23, 20, 3, 16, 2, 18, 10, 4, 4, 56, 389, 456, 461, 3205376763, 5, 23, 11, 56, 567, 407, 410, 2124022955, 95, 3, 0, 0, 4, 1, 0, 77, 97, 0, 2, 5, 61, 5, 3, 4, 3, 3, 3, 3, 3, 3, 3, 45465, 3, 2, 78, 97, 0, 4, 6, 47, 7, 159, 0, 1, 1, 0, 1, 2, 79, 96, 5, 3, 0, 6, 5, 0, 80, 43, 6, 5, 7, 0, 81, 97, 0, 6, 5, 56, 158, 470, 471, 1055874927, 33, 56, 164, 467, 470, 797628711, 4, 13, 46, 95, 6, 7, 0, 7, 8, 0, 97, 0, 9, 5, 84, 85, 86, 87, 42, 9, 8, 0, 55, 61, 9, 5, 6, 6, 5, 6, 5, 5, 5, 5, 33512, 6, 3, 88, 55, 63, 55, 56, 210, 504, 505, 3518192882, 54, 97, 0, 10, 8, 62, 7, 5, 5, 7, 8, 9, 7, 7, 7, 5, 61, 9, 6, 6, 5, 7, 7, 7, 7, 6, 7, 63764, 6, 3, 90, 55, 56, 247, 273, 274, 3069764117, 2, 55, 91, 63, 96, 8, 3, 0, 9, 11, 0, 97, 0, 12, 10, 92, 93, 94, 64, 64, 97, 0, 6, 8, 98, 8, 1429402918, 1134067273, 3788952303, 2148357739, 507526621, 3162352144, 1521292916, 4175493751, 2535067139, 894933672, 3549115996, 1908846023, 268231245, 2922473142, 1282011746, 3936640988, 2296300122, 655410883, 3310312882, 1669590405, 28862113, 2682985048, 1042722799, 3696769643, 2056619767, 416090669, 3070295434, 1430132383, 4084441748, 2444181431, 803361776, 1, 14, 57082, 25, 19, 24, 7, 56, 543, 388, 403, 1473363333, 25, 9, 10, 7, 56, 419, 562, 564, 3830279906, 11, 9, 22, 2, 56, 530, 418, 419, 2959897142, 24, 20, 5, 15, 5, 24, 0, 56, 388, 348, 363, 884861677, 32, 24, 7, 11, 7, 4, 23, 34, 2, 0, 25, 14, 7, 7, 12, 16, 5, 18, 16, 8, 2, 18, 5, 10, 11, 55, 8, 16, 1, 18, 20, 10, 4, 1, 16, 11, 24, 22, 55, 4, 56, 539, 400, 408, 3652244719, 8, 5, 9, 4, 32, 0, 13, 35, 23, 13, 22, 23, 10, 2, 55, 34, 4, 5, 9, 21, 20, 18, 23, 34, 24, 5, 56, 533, 389, 403, 1459612355, 13, 13, 22, 16, 56, 548, 396, 404, 922356307, 24, 21, 14, 23, 34, 6, 9, 56, 542, 560, 561, 2821963211, 55, 36, 4, 15, 56, 391, 421, 437, 1653398605, 56, 432, 428, 441, 2070020070, 35, 8, 18, 32, 22, 2, 17, 18, 23, 4, 3452190077, 1811583833, 170989348, 3709874382, 3627410361, 1986604812, 346206336, 3000768879, 1360232907, 4014995684, 2373687112, 733359043, 3387473758, 1747265745, 106811721, 2760931832, 1120846191, 3775092961, 2134389669, 494120871, 3148832929, 1507915967, 4162744746, 2522192273, 881393813, 3536216894, 1895047166, 254765946, 2909003938, 1268537732, 3922866471, 2282272899, 642072757, 3296579464, 1655731092, 15390879, 2670108817, 1877584598, 2847850002, 1206997556, 3861763632, 2221294093, 580439582, 3235336706, 1594504708, 4249333240, 2608011778, 967767565, 3621784090, 1981673413, 341117493, 2995303999, 1355160174, 4009452130, 21, 19, 15, 19, 8, 23, 4, 3, 34, 3, 15, 56, 541, 392, 407, 560394189, 56, 527, 554, 561, 145549988, 16, 21, 3, 0, 34, 14, 3, 56, 410, 395, 408, 1351911488, 20, 11, 11, 12, 12, 0, 6, 10, 16, 21, 15, 24, 24, 21, 21, 24, 4, 16]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 13; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = 100; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + case 63: + { + // JUMP_251 (specialized) + regs[fp + SLOTS.PC] = 251; + break; + } + case 64: + { + // RETURN_3 (specialized) + var retVal = regs[base + 3]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 65: + { + // LOAD_CONST_5_13_0 (specialized) + var dst = 5; + regs[base + dst] = this._constant(13, 0); + break; + } + case 66: + { + // LOAD_CONST_4_14_0 (specialized) + var dst = 4; + regs[base + dst] = this._constant(14, 0); + break; + } + case 67: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 68: + { + // LOAD_GLOBAL_6_3_0 (specialized) + var dst = 6; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 69: + { + // LOAD_CONST_7_15_0 (specialized) + var dst = 7; + regs[base + dst] = this._constant(15, 0); + break; + } + case 70: + { + // LOAD_GLOBAL_9_3_0 (specialized) + var dst = 9; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 71: + { + // LOAD_CONST_10_11_0 (specialized) + var dst = 10; + regs[base + dst] = this._constant(11, 0); + break; + } + case 72: + { + // GET_PROP_11_9_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 11; + var obj = regs[base + 9]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; + break; + } + case 73: + { + // LOAD_GLOBAL_9_3_0 (specialized) + var dst = 9; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 74: + { + // GET_PROP_12_9_10 (specialized) + // dst = regs[obj][regs[key]] + var dst = 12; + var obj = regs[base + 9]; + var key = regs[base + 10]; + regs[base + dst] = obj[key]; + break; + } + case 75: + { + // SET_PROP_6_7_9 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 6]; + var key = regs[base + 7]; + var val = regs[base + 9]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 76: + { + // RETURN_6 (specialized) + var retVal = regs[base + 6]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 77: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 78: + { + // LOAD_GLOBAL_5_3_0 (specialized) + var dst = 5; + var globalName = this._constant(3, 0); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case 79: + { + // SET_PROP_5_6_7 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 5]; + var key = regs[base + 6]; + var val = regs[base + 7]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 80: + { + // GET_PROP_7_5_6 (specialized) + // dst = regs[obj][regs[key]] + var dst = 7; + var obj = regs[base + 5]; + var key = regs[base + 6]; + regs[base + dst] = obj[key]; + break; + } + case 81: + { + // RETURN_2 (specialized) + var retVal = regs[base + 2]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 82: + { + // RETURN_5 (specialized) + var retVal = regs[base + 5]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 83: + { + // LOAD_UPVALUE_0_0 (specialized) + var dst = 0; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 84: + { + // LOAD_THIS_1 (specialized) + var dst = 1; + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case 85: + { + // FINALLY_SETUP_251_2_3_264 (specialized) + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: 251, + contReg: 2, + payloadReg: 3, + throwPad: 264 + }); + break; + } + case 86: + { + // TRY_SETUP_211_4 (specialized) + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: 211, + exceptionReg: 4 + }); + break; + } + case 87: + { + // LOAD_UPVALUE_8_0 (specialized) + var dst = 8; + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[0]._read(); + break; + } + case 88: + { + // LOAD_INT_2_265 (specialized) + var dst = 2; + regs[base + dst] = 265; + break; + } + case 89: + { + // JUMP_248 (specialized) + regs[fp + SLOTS.PC] = 248; + break; + } + case 90: + { + // LOAD_INT_2_266 (specialized) + var dst = 2; + regs[base + dst] = 266; + break; + } + case 91: + { + // LOAD_INT_2_267 (specialized) + var dst = 2; + regs[base + dst] = 267; + break; + } + case 92: + { + // SET_PROP_8_9_10 (specialized) + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + 8]; + var key = regs[base + 9]; + var val = regs[base + 10]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case 93: + { + // JUMP_REG_2 (specialized) + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + 2]; + break; + } + case 94: + { + // THROW_3 (specialized) + throw regs[base + 3]; + break; + } + case 95: + { + // LOAD_CONST,LOAD_CONST (macro) + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 96: + { + // LOAD_GLOBAL,LOAD_CONST (macro) + // LOAD_GLOBAL + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + // LOAD_CONST + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case 97: + { + // ALIAS_LOAD_CONST_2_1_0 (order: [2,1,0]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[2], _unsortedOperands[1], _unsortedOperands[0]]; + var dst = _operands[0]; + regs[base + dst] = this._constant(_operands[1], _operands[2]); + break; + } + case 98: + { + // ALIAS_RETURN_0 (order: [0]) + let _unsortedOperands = [this._operand()]; + let _operands = [_unsortedOperands[0]]; + var retVal = regs[base + _operands[0]]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case 61: + { + // ANTI_MOVE_9_0_3_5_6_12_1_7_11_2_4_10_8 (order: [9,0,3,5,6,12,1,7,11,2,4,10,8]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[1], _unsortedOperands[6], _unsortedOperands[9], _unsortedOperands[2], _unsortedOperands[10], _unsortedOperands[3], _unsortedOperands[4], _unsortedOperands[7], _unsortedOperands[12], _unsortedOperands[0], _unsortedOperands[11], _unsortedOperands[8], _unsortedOperands[5]]; + // NEQ + var dst = _operands[0]; + var a = regs[base + _operands[1]]; + regs[base + dst] = a !== regs[base + _operands[2]]; + // LOAD_INT + var dst = _operands[3]; + regs[base + dst] = _operands[4]; + // LTE + var dst = _operands[5]; + var a = regs[base + _operands[6]]; + regs[base + dst] = a <= regs[base + _operands[7]]; + // MOVE + var dst = _operands[8]; + regs[base + dst] = regs[base + _operands[9]]; + // BAND + var dst = _operands[10]; + var a = regs[base + _operands[11]]; + regs[base + dst] = a & regs[base + _operands[12]]; + break; + } + case 62: + { + // ANTI_UNARY_NEG_9_8_4_0_3_2_6_5_7_1 (order: [9,8,4,0,3,2,6,5,7,1]) + let _unsortedOperands = [this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand(), this._operand()]; + let _operands = [_unsortedOperands[3], _unsortedOperands[9], _unsortedOperands[5], _unsortedOperands[4], _unsortedOperands[2], _unsortedOperands[7], _unsortedOperands[6], _unsortedOperands[8], _unsortedOperands[1], _unsortedOperands[0]]; + // UNARY_NOT + var dst = _operands[0]; + regs[base + dst] = !regs[base + _operands[1]]; + // UNARY_NEG + var dst = _operands[2]; + regs[base + dst] = -regs[base + _operands[3]]; + // ADD + var dst = _operands[4]; + var a = regs[base + _operands[5]]; + regs[base + dst] = a + regs[base + _operands[6]]; + // BXOR + var dst = _operands[7]; + var a = regs[base + _operands[8]]; + regs[base + dst] = a ^ regs[base + _operands[9]]; + break; + } + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingTable.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingTable.js new file mode 100644 index 00000000..f4da483d --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/timingTable.js @@ -0,0 +1,1028 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"CLEANED", /* 3 */"SAVED", /* 4 */undefined, /* 5 */7, /* 6 */"pauseGate", /* 7 */1, /* 8 */true]; +var BYTECODE = [4, 1, 47, 4, 81, 0, 6, 0, 0, 5, 2, 4, 47, 4, 141, 0, 8, 1, 0, 1, 2, 5, 3, 4, 2, 4, 0, 0, 0, 5, 1, 0, 42, 6, 3, 0, 2, 7, 0, 0, 0, 8, 2, 0, 8, 9, 7, 8, 2, 7, 0, 0, 0, 8, 3, 0, 8, 10, 7, 8, 43, 8, 7, 10, 0, 48, 7, 3, 6, 9, 8, 9, 4, 5, 7, 0, 4, 4, 0, 45, 4, 4, 1, 0, 3, 5, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 3, 0, 47, 5, 136, 0, 1, 1, 0, 1, 2, 9, 3, 4, 5, 2, 3, 0, 0, 0, 4, 6, 0, 8, 5, 3, 4, 43, 4, 3, 5, 0, 45, 2, 0, 3, 4, 0, 45, 3, 3, 0, 0, 45, 0, 4, 1, 59, 193, 2, 3, 211, 54, 171, 4, 3, 5, 0, 42, 6, 5, 0, 55, 5, 3, 6, 1, 2, 213, 55, 39, 193, 55, 39, 187, 0, 5, 7, 0, 32, 6, 5, 5, 3, 6, 1, 2, 215, 55, 39, 193, 55, 1, 2, 217, 39, 193, 2, 5, 0, 0, 0, 6, 2, 0, 0, 7, 8, 0, 9, 5, 6, 7, 58, 2, 46, 3, 45, 3, 45, 3, 0, 5, 4, 0, 45, 5]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 11; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = 100; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + this[op](); + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var VMPrototype = VM.prototype; +VMPrototype[OP.LOAD_CONST] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = this._constant(); +}; +VMPrototype[OP.LOAD_INT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = this._operand(); +}; +VMPrototype[OP.LOAD_GLOBAL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; +}; +VMPrototype[OP.LOAD_UPVALUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); +}; +VMPrototype[OP.LOAD_THIS] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; +}; +VMPrototype[OP.MOVE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; +}; +VMPrototype[OP.STORE_GLOBAL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; +}; +VMPrototype[OP.STORE_UPVALUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); +}; +VMPrototype[OP.GET_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; +}; +VMPrototype[OP.SET_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); +}; +VMPrototype[OP.DELETE_PROP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; +}; +VMPrototype[OP.ADD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; +}; +VMPrototype[OP.SUB] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; +}; +VMPrototype[OP.MUL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; +}; +VMPrototype[OP.DIV] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; +}; +VMPrototype[OP.MOD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; +}; +VMPrototype[OP.EXP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); +}; +VMPrototype[OP.BAND] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; +}; +VMPrototype[OP.BOR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; +}; +VMPrototype[OP.BXOR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; +}; +VMPrototype[OP.SHL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; +}; +VMPrototype[OP.SHR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; +}; +VMPrototype[OP.USHR] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; +}; +VMPrototype[OP.LT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; +}; +VMPrototype[OP.GT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; +}; +VMPrototype[OP.LTE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; +}; +VMPrototype[OP.GTE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; +}; +VMPrototype[OP.EQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; +}; +VMPrototype[OP.NEQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; +}; +VMPrototype[OP.LOOSE_EQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; +}; +VMPrototype[OP.LOOSE_NEQ] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; +}; +VMPrototype[OP.IN] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; +}; +VMPrototype[OP.INSTANCEOF] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_NEG] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_POS] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_NOT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; +}; +VMPrototype[OP.UNARY_BITNOT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; +}; +VMPrototype[OP.TYPEOF] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; +}; +VMPrototype[OP.VOID] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + var dst = this._operand(); + this._operand(); // consumes argument (intended) + // consumes argument (intended) + regs[base + dst] = undefined; +}; +VMPrototype[OP.TYPEOF_SAFE] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; +}; +VMPrototype[OP.JUMP] = function () { + var fp = this._f; + var regs = this._regs; + regs[fp + SLOTS.PC] = this._operand(); +}; +VMPrototype[OP.JUMP_IF_FALSE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[OP.JUMP_IF_TRUE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; +}; +VMPrototype[OP.CALL] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } +}; +VMPrototype[OP.CALL_METHOD] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } +}; +VMPrototype[OP.NEW] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } +}; +VMPrototype[OP.RETURN] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; +}; +VMPrototype[OP.THROW] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + throw regs[base + this._operand()]; +}; +VMPrototype[OP.MAKE_CLOSURE] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + // unified prototype for new/instanceof + regs[base + dst] = shell; +}; +VMPrototype[OP.BUILD_ARRAY] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; +}; +VMPrototype[OP.BUILD_OBJECT] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; +}; +VMPrototype[OP.DEFINE_GETTER] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); +}; +VMPrototype[OP.DEFINE_SETTER] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); +}; +VMPrototype[OP.FOR_IN_SETUP] = function () { + var regs = this._regs; + var base = regs[this._f + SLOTS.REG_BASE]; + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; +}; +VMPrototype[OP.FOR_IN_NEXT] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } +}; +VMPrototype[OP.TRY_SETUP] = function () { + var fp = this._f; + var regs = this._regs; + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); +}; +VMPrototype[OP.TRY_END] = function () { + var fp = this._f; + var regs = this._regs; + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); +}; +VMPrototype[OP.FINALLY_SETUP] = function () { + var fp = this._f; + var regs = this._regs; + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); +}; +VMPrototype[OP.PATCH] = function () { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } +}; +VMPrototype[OP.JUMP_REG] = function () { + var fp = this._f; + var regs = this._regs; + var base = regs[fp + SLOTS.REG_BASE]; + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; +}; +VMPrototype[OP.DEBUGGER] = function () { + debugger; +}; +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/top-level-finally.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/top-level-finally.js new file mode 100644 index 00000000..18e66b47 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/top-level-finally.js @@ -0,0 +1,1019 @@ +var CONSTANTS = [/* 0 */"window", /* 1 */"TEST_OUTPUT", /* 2 */"CLEANED", /* 3 */true, /* 4 */"SAVED", /* 5 */undefined, /* 6 */7, /* 7 */"pauseGate"]; +var BYTECODE = [4, 1, 47, 5, 124, 0, 6, 0, 0, 5, 2, 5, 59, 39, 3, 4, 57, 2, 5, 0, 0, 0, 6, 1, 0, 42, 7, 2, 0, 9, 5, 6, 7, 55, 1, 3, 59, 39, 39, 2, 5, 0, 0, 0, 6, 2, 0, 0, 7, 3, 0, 9, 5, 6, 7, 58, 3, 46, 4, 2, 5, 0, 0, 0, 6, 1, 0, 2, 7, 0, 0, 0, 8, 1, 0, 8, 9, 7, 8, 2, 7, 0, 0, 0, 8, 2, 0, 8, 10, 7, 8, 2, 7, 0, 0, 0, 8, 4, 0, 8, 11, 7, 8, 43, 8, 7, 11, 0, 48, 7, 3, 9, 10, 8, 9, 5, 6, 7, 0, 5, 5, 0, 45, 5, 4, 1, 0, 3, 6, 0, 5, 2, 3, 2, 3, 0, 0, 0, 4, 4, 0, 47, 5, 179, 0, 1, 1, 0, 1, 2, 9, 3, 4, 5, 2, 3, 0, 0, 0, 4, 7, 0, 8, 5, 3, 4, 43, 4, 3, 5, 0, 45, 2, 0, 3, 5, 0, 45, 3, 3, 0, 0, 45, 0]; +var MAIN_START_PC = 0; +var MAIN_REG_COUNT = 12; +var ENCODE_BYTECODE = false; +var TIMING_CHECKS = false; +var OP = { + LOAD_CONST: 0, + LOAD_INT: 1, + LOAD_GLOBAL: 2, + LOAD_UPVALUE: 3, + LOAD_THIS: 4, + MOVE: 5, + STORE_GLOBAL: 6, + STORE_UPVALUE: 7, + GET_PROP: 8, + SET_PROP: 9, + DELETE_PROP: 10, + ADD: 11, + SUB: 12, + MUL: 13, + DIV: 14, + MOD: 15, + EXP: 60, + BAND: 16, + BOR: 17, + BXOR: 18, + SHL: 19, + SHR: 20, + USHR: 21, + LT: 22, + GT: 23, + LTE: 24, + GTE: 25, + EQ: 26, + NEQ: 27, + LOOSE_EQ: 28, + LOOSE_NEQ: 29, + IN: 30, + INSTANCEOF: 31, + UNARY_NEG: 32, + UNARY_POS: 33, + UNARY_NOT: 34, + UNARY_BITNOT: 35, + TYPEOF: 36, + VOID: 37, + TYPEOF_SAFE: 38, + JUMP: 39, + JUMP_IF_FALSE: 40, + JUMP_IF_TRUE: 41, + CALL: 42, + CALL_METHOD: 43, + NEW: 44, + RETURN: 45, + THROW: 46, + MAKE_CLOSURE: 47, + BUILD_ARRAY: 48, + BUILD_OBJECT: 49, + DEFINE_GETTER: 50, + DEFINE_SETTER: 51, + FOR_IN_SETUP: 52, + FOR_IN_NEXT: 53, + TRY_SETUP: 54, + TRY_END: 55, + PATCH: 56, + DEBUGGER: 57, + JUMP_REG: 58, + FINALLY_SETUP: 59 +}; +var SENTINELS = { + CALL_SPREAD: 65535 +}; +var SLOTS = { + PC: 0, + CALLER: 1, + RET_DST: 2, + THIS: 3, + CLOSURE: 4, + HANDLERS: 5, + FRAME_SIZE: 6, + REG_BASE: 7 +}; +var HEADER_SIZE = 8; +var FRAME_START = 1; +function base64ToBytes(s) { + return typeof Buffer !== "undefined" ? Buffer.from(s, "base64") : Uint8Array.from(atob(s), function (c) { + return c.charCodeAt(0); + }); +} +function decodeBytecode(s) { + if (!ENCODE_BYTECODE) return s; + var b = base64ToBytes(s); + // Each slot is a u32 stored as 4 little-endian bytes. + var r = new Uint32Array(b.length / 4); + for (var i = 0; i < r.length; i++) r[i] = (b[i * 4] | b[i * 4 + 1] << 8 | b[i * 4 + 2] << 16 | b[i * 4 + 3] << 24) >>> 0; + return r; +} + +// Closure map +// Maps shell functions -> inner Closure so the VM can fast-path instead of going through a sub-VM on internal calls. +// A WeakMap is used over a Symbol to prevent leaking information to debuggers +var CLOSURE_MAP = new WeakMap(); + +// Upvalue (Lua style) +// While the outer frame is alive: reads/writes go to vm._regs[_absSlot]. +// After the outer frame returns (closed): reads/writes hit this._value. +// (_absSlot is an absolute index into the flat slot array, so it stays valid +// no matter where the owning frame's block sits.) +function Upvalue(regs, absSlot) { + this._regs = regs; // shared reference to VM._regs flat array + this._absSlot = absSlot; // absolute index; stable as long as frame is alive + this._closed = false; + this._value = undefined; +} +Upvalue.prototype._read = function () { + return this._closed ? this._value : this._regs[this._absSlot]; +}; +Upvalue.prototype._write = function (v) { + if (this._closed) this._value = v;else this._regs[this._absSlot] = v; +}; +Upvalue.prototype._close = function () { + this._value = this._regs[this._absSlot]; + this._closed = true; +}; + +// Closure & Frame +function Closure(fn) { + this.fn = fn; + this.upvalues = []; + this.prototype = {}; // <- default prototype object for `new` +} + +// VM +// There is no Frame object and no frame stack. A frame is a block of slots in +// the flat array `_regs`: HEADER_SIZE header slots (see utils/frame-layout.ts) followed +// by the frame's registers. `_f` — the only piece of live execution state the VM +// object exposes — is the current frame's base offset, and each frame's CALLER +// slot points at the one that called it, so the call stack is an implicit linked +// list with nothing to enumerate. +function VM(bytecode, constants, globals) { + this.bytecode = bytecode; + this.constants = constants; + this.globals = globals; + // Open upvalues, keyed by absolute slot; created on the first capture so a + // closure-free program never allocates it. See captureUpvalue(). + this._openUpvalues = null; + + // Flat slot array (Lua-style register file, with the frame headers folded in). + // _regsTop is the next free slot (= base of the hypothetical next frame). + // On CALL: newBase = _regsTop; _regsTop += HEADER_SIZE + fn.regCount + // On RETURN: _regsTop = (pop the whole block) + // Slot 0 is never part of a frame: it holds the value run() hands back, and + // doubles as the "no frame" sentinel for _f / CALLER. + this._regs = []; + this._regsTop = FRAME_START; + this._f = 0; +} + +// Consume the next slot from the flat bytecode stream and advance the PC. +// Called by opcode handlers to read each of their operands in order. +// The PC is a header slot, so no object anywhere holds a property containing it. +VM.prototype._operand = function () { + return this.bytecode[this._regs[this._f + SLOTS.PC]++]; +}; + +// Push a new frame block on top of the slot array and make it current. +// retDst encodes the caller's destination register as (reg << 1), with bit 0 set +// for a constructor call (`new`) — RETURN uses it to decide whether to hand back +// the THIS slot instead of the returned value. +// args === null skips parameter setup (used for the root frame). +VM.prototype._pushFrame = function (closure, args, thisVal, retDst) { + var fn = closure.fn; + var regs = this._regs; + var fp = this._regsTop; + var size = HEADER_SIZE + fn.regCount; + var end = fp + size; + while (regs.length < end) regs.push(undefined); + for (var i = fp; i < end; i++) regs[i] = undefined; + var base = fp + HEADER_SIZE; + regs[fp + SLOTS.PC] = fn.startPc; + regs[fp + SLOTS.CALLER] = this._f; + regs[fp + SLOTS.RET_DST] = retDst; + regs[fp + SLOTS.THIS] = thisVal; + regs[fp + SLOTS.CLOSURE] = closure; + regs[fp + SLOTS.FRAME_SIZE] = size; + regs[fp + SLOTS.REG_BASE] = base; + + // Seeds for the decoy header slots this build happens to have (see utils/frame-layout.ts) + /* @NOISE */ + if (typeof SLOTS.NOISE_END === "number") regs[fp + SLOTS.NOISE_END] = end; + if (typeof SLOTS.NOISE_PARAMS === "number") regs[fp + SLOTS.NOISE_PARAMS] = fn.paramCount; + if (typeof SLOTS.NOISE_ARGS === "number") regs[fp + SLOTS.NOISE_ARGS] = args; + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = fn.startPc; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER] = 0; + this._regsTop = end; + if (args) { + if (fn.hasRest) { + var restSlot = fn.paramCount - 1; + for (var p = 0; p < restSlot; p++) regs[base + p] = p < args.length ? args[p] : undefined; + regs[base + restSlot] = args.slice(restSlot); + } else { + for (var a = 0; a < args.length && a < fn.regCount; a++) regs[base + a] = args[a]; + } + if (fn.paramCount < fn.regCount) regs[base + fn.paramCount] = args; + } + this._f = fp; +}; +VM.prototype.captureUpvalue = function (fp, slot) { + // Dedup by absolute slot — two closures capturing the same local share one + // Upvalue. _openUpvalues is keyed BY absolute slot rather than being a list of + // live captures, so this is a direct lookup instead of a scan, and closing a + // frame's captures costs a walk of that frame's own window (below) instead of + // a filter over every capture in the program. Entries are removed on close, so + // anything found here is open by construction. + var absSlot = this._regs[fp + SLOTS.REG_BASE] + slot; + var uvs = this._openUpvalues || (this._openUpvalues = []); + return uvs[absSlot] || (uvs[absSlot] = new Upvalue(this._regs, absSlot)); +}; + +// Reads and decodes a constant from the pool. +// idx: pool index (first operand of the constant pair emitted by resolveConstants). +// key: conceal key (second operand). 0 means no concealment. +// +// For integers: stored value is (original ^ key) with the full u32 key; XOR +// again to recover (JS `^` is int32, so this is symmetric). +// For strings: stored value is a base64 string containing u16 LE byte pairs. +// Mirrors decodeBytecode: base64 → bytes → u16 LE → XOR with a +// position-based Weyl keystream seeded by the full u32 key. +// idxIn, keyIn are passed in from specializedOpcodes when the operands are determined at compile time. +VM.prototype._constant = function (idxIn, keyIn) { + var idx = idxIn ?? this._operand(); + var key = keyIn ?? this._operand(); + var v = this.constants[idx]; + if (!key) return v; + if (typeof v === "number") return v ^ key; + if (typeof v !== "string") return v; + + // String: base64-decode to u16 LE byte pairs, then XOR each code with a + // 16-bit keystream word derived from the full 32-bit key + position. + var b = base64ToBytes(v); + var out = ""; + var k = key; + for (var i = 0; i < b.length / 2; i++) { + k = k + 0x9e3779b9 | 0; // 32-bit Weyl step (position-based) + var ks = (k ^ k >>> 13) & 0xffff; // 16-bit keystream word + var code = b[i * 2] | b[i * 2 + 1] << 8; // u16 LE + out += String.fromCharCode(code ^ ks); + } + return out; +}; +VM.prototype._closeUpvaluesFor = function (fp) { + // Called on RETURN — close every upvalue captured from this frame's register + // window [REG_BASE, end of the frame block) and free its table entry, so a + // later frame reusing those slots starts with no captures. + var uvs = this._openUpvalues; + if (!uvs) return; // nothing in this program ever captured a local + var regs = this._regs; + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = regs[fp + SLOTS.REG_BASE]; i < hi; i++) { + var uv = uvs[i]; + if (uv) { + uv._close(); + uvs[i] = undefined; + } + } +}; + +// Executes `closure` to completion and returns its return value. +// The root frame's CALLER slot is 0 (the sentinel), so its RETURN parks the +// value in slot 0 and clears _f, which stops the loop below. +VM.prototype.run = function (closure, thisVal, args) { + var now = () => { + return performance.now(); + }; + var lastTime = now(); + this._pushFrame(closure, args, thisVal, 0); + while (true) { + var fp = this._f; + var regs = this._regs; + var bc = this.bytecode; + var pc = regs[fp + SLOTS.PC]; + if (pc >= bc.length) break; + regs[fp + SLOTS.PC] = pc + 1; + var op = this.bytecode[pc]; + // var opcode = this.bytecode[pc]; + // console.log(`[run] pc=${pc}, opcode=${opcode}, name=${Object.keys(OP).find((key) => OP[key] === opcode)}`); + + // Churn the decoy header slots so the real PC isn't the only slot moving + /* @NOISE */ + if (typeof SLOTS.NOISE_PC === "number") regs[fp + SLOTS.NOISE_PC] = (regs[fp + SLOTS.NOISE_PC] + 1) % bc.length; + if (typeof SLOTS.NOISE_COUNTER === "number") regs[fp + SLOTS.NOISE_COUNTER]++; + if (typeof SLOTS.NOISE_MIRROR === "number") regs[fp + SLOTS.NOISE_MIRROR] = op; + if (typeof SLOTS.NOISE_ACC === "number") regs[fp + SLOTS.NOISE_ACC] = regs[fp + SLOTS.NOISE_ACC] ^ pc | 0; + + // Debugging protection: Detects debugger by checking for >1s pauses which can only happen from debugger; or extremely slow sync tasks + if (TIMING_CHECKS) { + var currentTime = now(); + var isTamper = currentTime - lastTime > TIMING_CHECKS; + lastTime = currentTime; + if (isTamper) { + // Poison the bytecode + for (var i = 0; i < this.bytecode.length; i++) this.bytecode[i] = 0; + // Break the current state + for (var i2 = fp; i2 < this._regsTop; i2++) this._regs[i2] = undefined; + op = OP.JUMP; + regs[fp + SLOTS.PC] = this.bytecode.length; // jump past end to halt + } + } + try { + var base = regs[fp + SLOTS.REG_BASE]; + + /* @SWITCH */ + switch (op) { + case OP.LOAD_CONST: + { + var dst = this._operand(); + regs[base + dst] = this._constant(); + break; + } + case OP.LOAD_INT: + { + var dst = this._operand(); + regs[base + dst] = this._operand(); + break; + } + case OP.LOAD_GLOBAL: + { + var dst = this._operand(); + var globalName = this._constant(); + if (!(globalName in this.globals)) { + throw new ReferenceError(`${globalName} is not defined`); + } + regs[base + dst] = this.globals[globalName]; + break; + } + case OP.LOAD_UPVALUE: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.CLOSURE].upvalues[this._operand()]._read(); + break; + } + case OP.LOAD_THIS: + { + var dst = this._operand(); + regs[base + dst] = regs[fp + SLOTS.THIS]; + break; + } + case OP.MOVE: + { + var dst = this._operand(); + regs[base + dst] = regs[base + this._operand()]; + break; + } + case OP.STORE_GLOBAL: + { + // globals[globalName] = regs[src] + this.globals[this._constant()] = regs[base + this._operand()]; + break; + } + case OP.STORE_UPVALUE: + { + var uvIdx = this._operand(); + regs[fp + SLOTS.CLOSURE].upvalues[uvIdx]._write(regs[base + this._operand()]); + break; + } + case OP.GET_PROP: + { + // dst = regs[obj][regs[key]] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = obj[key]; + break; + } + case OP.SET_PROP: + { + // regs[obj][regs[key]] = regs[val] + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + // Reflect.set performs [[Set]] without throwing on failure (non-strict mode behavior) + Reflect.set(obj, key, val); + break; + } + case OP.DELETE_PROP: + { + // regs[dst] = delete regs[obj][regs[key]] + // The delete operator returns true if successful which is most cases + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + regs[base + dst] = delete obj[key]; + break; + } + + // Arithmetic (dst, src1, src2) + case OP.ADD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a + regs[base + this._operand()]; + break; + } + case OP.SUB: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a - regs[base + this._operand()]; + break; + } + case OP.MUL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a * regs[base + this._operand()]; + break; + } + case OP.DIV: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a / regs[base + this._operand()]; + break; + } + case OP.MOD: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a % regs[base + this._operand()]; + break; + } + case OP.EXP: + { + // Math.pow instead of `**` + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = Math.pow(a, regs[base + this._operand()]); + break; + } + case OP.BAND: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a & regs[base + this._operand()]; + break; + } + case OP.BOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a | regs[base + this._operand()]; + break; + } + case OP.BXOR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a ^ regs[base + this._operand()]; + break; + } + case OP.SHL: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a << regs[base + this._operand()]; + break; + } + case OP.SHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >> regs[base + this._operand()]; + break; + } + case OP.USHR: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >>> regs[base + this._operand()]; + break; + } + + // Comparison (dst, src1, src2) + case OP.LT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a < regs[base + this._operand()]; + break; + } + case OP.GT: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a > regs[base + this._operand()]; + break; + } + case OP.LTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a <= regs[base + this._operand()]; + break; + } + case OP.GTE: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a >= regs[base + this._operand()]; + break; + } + case OP.EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a === regs[base + this._operand()]; + break; + } + case OP.NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a !== regs[base + this._operand()]; + break; + } + case OP.LOOSE_EQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a == regs[base + this._operand()]; + break; + } + case OP.LOOSE_NEQ: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a != regs[base + this._operand()]; + break; + } + case OP.IN: + { + var dst = this._operand(); + var a = regs[base + this._operand()]; + regs[base + dst] = a in regs[base + this._operand()]; + break; + } + case OP.INSTANCEOF: + { + // regs[dst] = regs[obj] instanceof regs[ctor] + // Since VM closures are wrapped in native function shells (MAKE_CLOSURE), the native operator works + var dst = this._operand(); + var obj = regs[base + this._operand()]; + regs[base + dst] = obj instanceof regs[base + this._operand()]; + break; + } + + // Unary (dst, src) + case OP.UNARY_NEG: + { + var dst = this._operand(); + regs[base + dst] = -regs[base + this._operand()]; + break; + } + case OP.UNARY_POS: + { + var dst = this._operand(); + regs[base + dst] = +regs[base + this._operand()]; + break; + } + case OP.UNARY_NOT: + { + var dst = this._operand(); + regs[base + dst] = !regs[base + this._operand()]; + break; + } + case OP.UNARY_BITNOT: + { + var dst = this._operand(); + regs[base + dst] = ~regs[base + this._operand()]; + break; + } + case OP.TYPEOF: + { + var dst = this._operand(); + regs[base + dst] = typeof regs[base + this._operand()]; + break; + } + case OP.VOID: + { + var dst = this._operand(); + this._operand(); // consumes argument (intended) + regs[base + dst] = undefined; + break; + } + case OP.TYPEOF_SAFE: + { + // regs[dst] = typeof window[name] + // Never throws ReferenceError, instead returns undefined for undeclared variables + var dst = this._operand(); + var name = this._constant(); + var val = Object.prototype.hasOwnProperty.call(this.globals, name) ? this.globals[name] : undefined; + regs[base + dst] = typeof val; + break; + } + + // Control flow + case OP.JUMP: + regs[fp + SLOTS.PC] = this._operand(); + break; + case OP.JUMP_IF_FALSE: + { + var src = this._operand(); + var target = this._operand(); + if (!regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + case OP.JUMP_IF_TRUE: + { + // || short-circuit: if truthy, jump over RHS. + var src = this._operand(); + var target = this._operand(); + if (regs[base + src]) regs[fp + SLOTS.PC] = target; + break; + } + + // Calls + case OP.CALL: + { + // dst, calleeReg, argc, [argReg...] (argc=-1 means next operand is spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, this.globals, dst << 1); + } else { + regs[base + dst] = callee.apply(null, args); + } + break; + } + case OP.CALL_METHOD: + { + // dst, receiverReg, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var receiver = regs[base + this._operand()]; + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + this._pushFrame(closure, args, receiver, dst << 1); + } else { + regs[base + dst] = callee.apply(receiver, args); + } + break; + } + case OP.NEW: + { + // dst, calleeReg, argc, [argReg...] (argc=SENTINELS.CALL_SPREAD means spread-args array reg) + var dst = this._operand(); + var callee = regs[base + this._operand()]; + var argc = this._operand(); + var args; + if (argc === SENTINELS.CALL_SPREAD) { + args = regs[base + this._operand()]; + } else { + args = new Array(argc); + for (var i = 0; i < argc; i++) args[i] = regs[base + this._operand()]; + } + var closure = callee && CLOSURE_MAP.get(callee); + if (closure) { + // The new object doubles as the frame's THIS; bit 0 of RET_DST marks + // the frame as a constructor call so RETURN can fall back to it. + this._pushFrame(closure, args, Object.create(closure.prototype || null), dst << 1 | 1); + } else { + // Reflect.construct is required - Object.create+apply does NOT set + // internal slots ([[NumberData]], [[StringData]], etc.) for built-ins. + regs[base + dst] = Reflect.construct(callee, args); + } + break; + } + case OP.RETURN: + { + var retVal = regs[base + this._operand()]; + this._closeUpvaluesFor(fp); // must happen before frame is abandoned + + var caller = regs[fp + SLOTS.CALLER]; + var retDst = regs[fp + SLOTS.RET_DST]; + + // NewExpression: When invoking from the 'new' keyword, the newly constructed object is returned instead (if the original function doesn't return an object) + if (retDst & 1 && (typeof retVal !== "object" || retVal === null)) retVal = regs[fp + SLOTS.THIS]; + + // Zero out the callee's whole block (header included) to limit + // exposing runtime values, then hand the slots back. + var hi = fp + regs[fp + SLOTS.FRAME_SIZE]; + for (var i = fp; i < hi; i++) regs[i] = undefined; + this._regsTop = fp; + this._f = caller; + + // A caller of 0 is the root frame returning to the host: park the + // value in slot 0 for run() to hand back. A bare `return retVal` + // would only exit an extracted VM.prototype[op] handler function + // (handlerTable option), not the loop. Keeping the sole `break` + // trailing (no mid-body break/return) also lets the handlerTable + // transform lift this body verbatim. + regs[caller ? regs[caller + SLOTS.REG_BASE] + (retDst >> 1) : 0] = retVal; + break; + } + case OP.THROW: + throw regs[base + this._operand()]; + + // Closures + case OP.MAKE_CLOSURE: + { + // dst, startPc, paramCount, regCount, uvCount, hasRest, [isLocal, idx, ...] + var dst = this._operand(); + var startPc = this._operand(); + var paramCount = this._operand(); + var regCount = this._operand(); + var uvCount = this._operand(); + var hasRest = this._operand(); // 1 if last param is a rest element + + var uvDescs = new Array(uvCount); + for (var i = 0; i < uvCount; i++) { + var isLocalRaw = this._operand(); + var uvIndex = this._operand(); + uvDescs[i] = { + isLocal: isLocalRaw, + _index: uvIndex + }; + } + + // uvDescs is consumed by the capture loop below and then dropped — it + // is deliberately NOT kept on the descriptor. A closure that carries + // its capture plan around spells out the closure graph ("captures + // local 2 of the parent frame") to anything that gets hold of it. + var fn = { + paramCount: paramCount, + regCount: regCount, + startPc: startPc, + hasRest: hasRest + }; + var closure = new Closure(fn); + for (var i = 0; i < uvDescs.length; i++) { + var uvd = uvDescs[i]; + if (uvd.isLocal) { + closure.upvalues.push(this.captureUpvalue(fp, uvd._index)); + } else { + closure.upvalues.push(regs[fp + SLOTS.CLOSURE].upvalues[uvd._index]); + } + } + + // Wrap in a native callable shell so host code (array methods, + // test assertions, setTimeout, etc.) can invoke VM closures. + // CLOSURE_MAP lets VM-internal CALL/NEW bypass the sub-VM entirely. + var self = this; + var shell = function (c) { + return function () { + return new VM(self.bytecode, self.constants, self.globals).run(c, this == null ? self.globals : this, Array.prototype.slice.call(arguments)); + }; + }(closure); + CLOSURE_MAP.set(shell, closure); + shell.prototype = closure.prototype; // unified prototype for new/instanceof + regs[base + dst] = shell; + break; + } + + // Collections + case OP.BUILD_ARRAY: + { + // dst, count, [elemReg...] + var dst = this._operand(); + var count = this._operand(); + var elems = new Array(count); + for (var i = 0; i < count; i++) elems[i] = regs[base + this._operand()]; + regs[base + dst] = elems; + break; + } + case OP.BUILD_OBJECT: + { + // dst, pairCount, [keyReg, valReg, ...] + var dst = this._operand(); + var pairCount = this._operand(); + var o = {}; + for (var i = 0; i < pairCount; i++) { + var key = regs[base + this._operand()]; + var val = regs[base + this._operand()]; + o[key] = val; + } + regs[base + dst] = o; + break; + } + + // Object methods (getters / setters) + case OP.DEFINE_GETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var getterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var getDesc = { + get: getterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.set === "function") { + getDesc.set = existingDesc.set; + } + Object.defineProperty(obj, key, getDesc); + break; + } + case OP.DEFINE_SETTER: + { + // obj, key, fn + var obj = regs[base + this._operand()]; + var key = regs[base + this._operand()]; + var setterFn = regs[base + this._operand()]; + var existingDesc = Object.getOwnPropertyDescriptor(obj, key); + var setDesc = { + set: setterFn, + configurable: true, + enumerable: true + }; + if (existingDesc && typeof existingDesc.get === "function") { + setDesc.get = existingDesc.get; + } + Object.defineProperty(obj, key, setDesc); + break; + } + + // ── For-in iteration ────────────────────────────────────────────────── + case OP.FOR_IN_SETUP: + { + // dst, src — build iterator object from enumerable keys of regs[src] + var dst = this._operand(); + var obj = regs[base + this._operand()]; + var keys = []; + if (obj !== null && obj !== undefined) { + var seen = Object.create(null); + var cur = Object(obj); // box primitives + while (cur !== null) { + var ownNames = Object.getOwnPropertyNames(cur); + for (var i = 0; i < ownNames.length; i++) { + var k = ownNames[i]; + if (!(k in seen)) { + seen[k] = true; + var propDesc = Object.getOwnPropertyDescriptor(cur, k); + if (propDesc && propDesc.enumerable) { + keys.push(k); + } + } + } + cur = Object.getPrototypeOf(cur); + } + } + regs[base + dst] = { + _keys: keys, + i: 0 + }; + break; + } + case OP.FOR_IN_NEXT: + { + // dst, iterReg, exitTarget + // Advances iterator; writes next key to dst, or jumps to exitTarget when done. + var dst = this._operand(); + var iter = regs[base + this._operand()]; + var exitTarget = this._operand(); + if (iter.i >= iter._keys.length) { + regs[fp + SLOTS.PC] = exitTarget; + } else { + regs[base + dst] = iter._keys[iter.i++]; + } + break; + } + + // ── Exception handling ──────────────────────────────────────────────── + case OP.TRY_SETUP: + { + // handlerPc, exceptionReg — push exception handler record onto current + // frame. The handler stack lives in a header slot and is created on + // first use, so frames that never try/catch carry nothing. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + handlerPc: this._operand(), + exceptionReg: this._operand() + }); + break; + } + case OP.TRY_END: + { + // Normal exit from a try block — disarm the top handler record + // (works for both catch and finally regions; they share the stack). + regs[fp + SLOTS.HANDLERS].pop(); + break; + } + case OP.FINALLY_SETUP: + { + // finallyPc, contReg, payloadReg, throwPad + // Arm a finalizer for the current region. Unlike a catch record this + // carries no exceptionReg; instead the continuation register (contReg) + // receives the resume PC and payloadReg carries the in-flight value. + var hs = regs[fp + SLOTS.HANDLERS]; + if (!hs) regs[fp + SLOTS.HANDLERS] = hs = []; + hs.push({ + finallyPc: this._operand(), + contReg: this._operand(), + payloadReg: this._operand(), + throwPad: this._operand() + }); + break; + } + + // Self-modifying bytecode + case OP.PATCH: + { + // destPc, sliceStart, sliceEnd, key + var destPc = this._operand(); + var sliceStart = this._operand(); + var sliceEnd = this._operand(); + var pk = this._operand() ^ destPc | 0; + for (var pi = sliceStart; pi < sliceEnd; pi++) { + pk = pk + 0x9e3779b9 | 0; + // >>> 0: a negative slot would never match a case in the switch. + this.bytecode[destPc + (pi - sliceStart)] = (this.bytecode[pi] ^ (pk ^ pk >>> 13)) >>> 0; + } + break; + } + case OP.JUMP_REG: + { + // Indirect jump: allows VM to jump based on runtime values. + regs[fp + SLOTS.PC] = regs[base + this._operand()]; + break; + } + case OP.DEBUGGER: + { + debugger; + break; + } + default: + throw new Error("Unknown opcode: " + op + " at pc " + (regs[fp + SLOTS.PC] - 1)); + } + } catch (err) { + // Exception handler unwinding + // Walk the CALLER chain from the current frame until we find one holding an open exception handler (TRY_SETUP without a matching TRY_END). + // For every frame we abandon along the way, close its captured upvalues and release its block. + var handledFrame = 0; + var searchFrame = this._f; + while (searchFrame) { + var handlers = regs[searchFrame + SLOTS.HANDLERS]; + if (handlers && handlers.length > 0) { + handledFrame = searchFrame; + break; + } + // No handler in this frame — abandon it and walk up. + this._closeUpvaluesFor(searchFrame); + this._regsTop = searchFrame; + searchFrame = regs[searchFrame + SLOTS.CALLER]; + this._f = searchFrame; + } + if (!handledFrame) throw err; // if there's no handler, propagate back to host + + var h = regs[handledFrame + SLOTS.HANDLERS].pop(); + var hBase = regs[handledFrame + SLOTS.REG_BASE]; + if (h.exceptionReg !== undefined) { + // catch region — deliver the exception to the catch binding and run it. + regs[hBase + h.exceptionReg] = err; + regs[handledFrame + SLOTS.PC] = h.handlerPc; + } else { + // finally region: run the finalizer with the exception pending, then + // resume at its throw pad (which re-raises and continues unwinding). + regs[hBase + h.contReg] = h.throwPad; + regs[hBase + h.payloadReg] = err; + regs[handledFrame + SLOTS.PC] = h.finallyPc; + } + // Walking the chain already released every frame pushed inside the + // protected region; the handling frame is now the top of the stack again. + this._regsTop = handledFrame + regs[handledFrame + SLOTS.FRAME_SIZE]; + this._f = handledFrame; + } + + // RETURN of the root frame cleared _f — stop the loop and hand the value + // back to run()'s caller (host code, or the sub-VM shell) from slot 0. + if (!this._f) return this._regs[0]; + } +}; + +/* @BOOT */ // <- This comment can't be removed! +var globals = globalThis; +if (typeof window !== "undefined") { + globals.window = window; + globals.document = typeof document !== "undefined" ? document : undefined; +} +if (typeof module !== "undefined") { + globals.module = module; + globals.exports = typeof exports !== "undefined" ? exports : undefined; +} +var vm = new VM(decodeBytecode(BYTECODE), CONSTANTS, globals); +vm.run(new Closure({ + paramCount: 0, + regCount: MAIN_REG_COUNT, + startPc: MAIN_START_PC +}), undefined, null // no arguments object / parameter setup for the root frame +); \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/top-level-finally.src.js b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/top-level-finally.src.js new file mode 100644 index 00000000..4f7f0623 --- /dev/null +++ b/test/vm/jsconfuser-vm/fixtures/runtime-finalizers/top-level-finally.src.js @@ -0,0 +1 @@ +function work() { let value = 7; window.SAVED = () => value; window.pauseGate(); return value; } try { window.TEST_OUTPUT = work(); } finally { window.CLEANED = true; } window.TEST_OUTPUT = [window.TEST_OUTPUT, window.CLEANED, window.SAVED()]; \ No newline at end of file diff --git a/test/vm/jsconfuser-vm/normalize-concealed-constants.test.js b/test/vm/jsconfuser-vm/normalize-concealed-constants.test.js new file mode 100644 index 00000000..fdf5539f --- /dev/null +++ b/test/vm/jsconfuser-vm/normalize-concealed-constants.test.js @@ -0,0 +1,122 @@ +import fs from 'node:fs' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnoseNormalizeConcealedConstants } from '../../../src/vm/jsconfuser-vm/normalize-concealed-constants.js' +import { readWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { validateReferences } from '../../../src/vm/jsconfuser-vm/validate-references.js' +import { diagnoseStandalone } from '../../../src/vm/jsconfuser-vm/decode-standalone.js' +import { + assertDerivedStateConsistency, + assertNoVmResidue, + parseSuccessfulOutput, +} from './integration/harness.js' + +function fixture(name, file) { + return fs.readFileSync( + new URL(`fixtures/concealed-constants-${name}/${file}`, import.meta.url), + 'utf8', + ) +} + +function observe(script) { + const window = {} + vm.runInNewContext( + script, + { window, Buffer, performance: { now: () => 0 } }, + { timeout: 1000 }, + ) + return JSON.parse(JSON.stringify(window.TEST_OUTPUT)) +} + +test.each(['arithmetic', 'closures', 'exceptions', 'repeatedArrays'])( + 'proves, normalizes, and decodes concealed constants in %s', + (name) => { + const source = fixture(name, 'source.js') + const encoded = fixture(name, 'encoded.js') + const qualification = JSON.parse(fixture(name, 'qualification.json')) + const extracted = diagnoseContainer(encoded) + expect(extracted.ok).toBe(true) + const wordcode = readWordcode(extracted.result) + expect(wordcode).not.toBeNull() + expect(validateReferences(extracted.result, wordcode)).toBeNull() + const before = JSON.stringify(extracted.result) + const normalized = diagnoseNormalizeConcealedConstants( + encoded, + extracted.result, + wordcode, + ) + expect(normalized.ok).toBe(true) + expect(normalized.result.concealedPoolEntries).toBeGreaterThan(0) + expect(JSON.stringify(extracted.result)).toBe(before) + const canonical = readWordcode(normalized.result.container) + expect(canonical).not.toBeNull() + expect( + validateReferences(normalized.result.container, canonical), + ).not.toBeNull() + + const decoded = diagnoseStandalone(encoded) + expect(decoded.ok).toBe(true) + const output = parseSuccessfulOutput(decoded.result.output) + expect(assertNoVmResidue(output).ok).toBe(true) + expect( + assertDerivedStateConsistency(output, decoded.result.output).ok, + ).toBe(true) + expect(observe(encoded)).toEqual(observe(source)) + expect(observe(decoded.result.output)).toEqual(observe(source)) + expect(qualification.profile).toBe('concealConstants') + expect(qualification.effectObservation).toEqual({ + axis: 'serialized-bytecode', + observed: true, + }) + }, +) + +test('declines malformed base64 and inconsistent keys without changing the container', () => { + const encoded = fixture('exceptions', 'encoded.js') + const extracted = diagnoseContainer(encoded).result + const wordcode = readWordcode(extracted) + const concealedString = wordcode.instructions + .flatMap((instruction) => instruction.operands) + .find( + (operand) => + operand.kind === 'constant-ref' && + operand.concealKey !== 0 && + typeof extracted.roles.pool.values[operand.index] === 'string', + ) + expect(concealedString).toBeDefined() + const malformed = structuredClone(extracted) + malformed.roles.pool.values[concealedString.index] = '%%%%' + expect( + diagnoseNormalizeConcealedConstants(encoded, malformed, wordcode), + ).toMatchObject({ + ok: false, + diagnostic: { code: 'concealed-string' }, + }) + + const duplicate = wordcode.instructions + .flatMap((instruction) => + instruction.operands + .filter((operand) => operand.kind === 'constant-ref') + .map((operand) => ({ instruction, operand })), + ) + .filter( + ({ operand }) => + operand.index === concealedString.index && + operand.concealKey === concealedString.concealKey, + )[1] + expect(duplicate).toBeDefined() + const changed = structuredClone(extracted) + const keyPc = + duplicate.instruction.pc + + (duplicate.instruction.name === 'STORE_GLOBAL' ? 2 : 3) + changed.roles.words.values[keyPc] = + (changed.roles.words.values[keyPc] + 1) >>> 0 + const changedWordcode = readWordcode(changed) + expect( + diagnoseNormalizeConcealedConstants(encoded, changed, changedWordcode), + ).toMatchObject({ + ok: false, + diagnostic: { code: 'concealed-key' }, + }) +}) diff --git a/test/vm/jsconfuser-vm/patch-added-opcodes.test.js b/test/vm/jsconfuser-vm/patch-added-opcodes.test.js new file mode 100644 index 00000000..a8662a15 --- /dev/null +++ b/test/vm/jsconfuser-vm/patch-added-opcodes.test.js @@ -0,0 +1,142 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import generate from '@babel/generator' +import * as t from '@babel/types' +import { diagnoseContainer } from '../../../src/vm/jsconfuser-vm/extract-container.js' +import { diagnosePatchAddedOpcodes } from '../../../src/vm/jsconfuser-vm/observe-patch-added-opcodes.js' +import { diagnoseSelfModifyingRegions } from '../../../src/vm/jsconfuser-vm/observe-self-modifying.js' +import { diagnoseNormalizeSelfModifyingWordcode } from '../../../src/vm/jsconfuser-vm/normalize-self-modifying-wordcode.js' +import { diagnoseWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' +import { decodeJsconfuserVm } from '../../../src/plugin/jsconfuser-vm.js' +import { patchAddedOpcodeCensus, assertNamedPatchDiagnostic } from '../../../scripts/qualify-jsconfuser-vm/patch.mjs' +import { observeKnown } from '../../../scripts/qualify-jsconfuser-vm/worker.mjs' +import { exactEqual } from '../../../scripts/qualify-jsconfuser-vm/codec.mjs' +import { outputChecks } from '../../../scripts/qualify-jsconfuser-vm/oracles.mjs' + +const fixture = (name, suffix = '.js') => fs.readFileSync(new URL(`fixtures/patch-added-opcodes/${name}${suffix}`, import.meta.url), 'utf8') +const replaceWords = (source, words) => source.replace(/^var BYTECODE = \[[^\n]+\];$/m, `var BYTECODE = ${JSON.stringify(words)};`) + +for (const [name, kind] of [['alias', 'alias'], ['macro', 'macro'], ['specialized', 'specialized'], ['anti', 'anti'], ['stack', null], ['finally', null], ['arrays', null]]) { + test(`recovers reached PATCH ${name} widths, output and consumed records`, () => { + const source = fixture(name) + const container = diagnoseContainer(source).result + const original = JSON.stringify(container) + const addedOpcodes = diagnosePatchAddedOpcodes(source) + expect(addedOpcodes.ok).toBe(true) + const observed = diagnoseSelfModifyingRegions(container, addedOpcodes.result) + expect(observed.ok).toBe(true) + const census = patchAddedOpcodeCensus(source) + expect(census.ok).toBe(true) + expect(census.counts.selfModifying).toBeGreaterThan(0) + if (kind) { + expect(addedOpcodes.result[kind]).not.toBeNull() + expect(census.patchedKinds).toContain({ alias: 'ALIAS', macro: 'MACRO', specialized: 'SPECIALIZED', anti: 'SYNTHETIC' }[kind]) + } else { + for (const [option, count] of Object.entries(census.counts)) + if (name !== 'arrays' || option !== 'antiInstrumentation') expect(count).toBeGreaterThan(0) + if (name === 'arrays') { + const macros = observed.result.instructions.filter(instruction => instruction.name === 'MACRO' && instruction.steps.some(step => step.name === 'BUILD_ARRAY')) + expect(new Set(macros.map(instruction => instruction.width)).size).toBeGreaterThan(1) + expect(new Set(macros.flatMap(instruction => instruction.steps.filter(step => step.name === 'BUILD_ARRAY').map(step => step.words[2])))).toEqual(new Set([4, 6])) + } else expect(census.patchedKinds).toEqual(expect.arrayContaining(['ALIAS', 'MACRO', 'SPECIALIZED', 'SYNTHETIC'])) + } + const normalized = diagnoseNormalizeSelfModifyingWordcode(container, { source }) + expect(normalized.ok).toBe(true) + const result = normalized.result + // Added opcodes stay for their own stages; canonical readWordcode output exists once none remains. + const words = diagnoseWordcode(result.container) + expect(result.wordcode).toEqual(words.ok ? words.result : null) + expect(result.wordCount).toBeLessThan(result.originalWordCount) + expect(JSON.stringify(container)).toBe(original) + const current = decodeJsconfuserVm(source) + expect(current.ok).toBe(true) + expect(current.output).not.toBe(source) + expect(outputChecks(current.output, source, current.result).ok).toBe(true) + expect(exactEqual(observeKnown(current.output), observeKnown(fixture(name, '.src.js')))).toBe(true) + expect(exactEqual(observeKnown(source), observeKnown(fixture(name, '.src.js')))).toBe(true) + expect(current.result.finalJavaScriptSemantics).toBe(false) + }) + if (kind) test(`PATCH ${kind} reader extension is load bearing`, () => { + const source = fixture(name) + const container = diagnoseContainer(source).result + const addedOpcodes = diagnosePatchAddedOpcodes(source).result + const removed = diagnoseSelfModifyingRegions(container, { ...addedOpcodes, [kind]: null }) + assertNamedPatchDiagnostic(removed, 'patch-ciphertext') + expect(removed.result).toBeNull() + expect(diagnoseWordcode(container).ok).toBe(false) + }) +} + +test('normal-source interval and ciphertext damage reach named atomic refusals', () => { + const source = fixture('stack') + const container = diagnoseContainer(source).result + const addedOpcodes = diagnosePatchAddedOpcodes(source).result + const patch = diagnoseSelfModifyingRegions(container, addedOpcodes).result.patches[0] + for (const [code, offset, mutate] of [ + ['patch-ciphertext', patch.sourceStart, value => (value ^ 0xffffffff) >>> 0], + ]) { + const words = container.roles.words.values.slice() + words[offset] = mutate(words[offset]) + const damaged = replaceWords(source, words) + expect(damaged).not.toBe(source) + const admitted = diagnoseContainer(damaged) + expect(admitted.ok).toBe(true) + const refused = diagnoseNormalizeSelfModifyingWordcode(admitted.result, { source: damaged }) + assertNamedPatchDiagnostic(refused, code) + const current = decodeJsconfuserVm(damaged) + expect(current).toMatchObject({ ok: false, output: damaged }) + } +}) + +// Opcodes are named by what their handlers do, never by their values: moving an added handler to +// another label leaves the words that use it without a handler, and the decode declines. +test('a moved added handler label leaves its PATCH words without a handler', () => { + const source = fixture('alias') + const damaged = source.replace('case 61:', 'case 4294967295:') + expect(damaged).not.toBe(source) + expect(diagnoseContainer(damaged).ok).toBe(true) + expect(decodeJsconfuserVm(damaged)).toMatchObject({ ok: false, output: damaged, diagnostic: { code: 'wordcode-declined' } }) +}) + +test('named damage and behavior oracles fail on counterexamples', () => { + expect(() => assertNamedPatchDiagnostic({ ok: true }, 'patch-ciphertext')).toThrow('Expected PATCH mechanism') + expect(() => assertNamedPatchDiagnostic({ ok: false, diagnostic: { code: 'patch-destination' } }, 'patch-ciphertext')).toThrow('observed patch-destination') + const source = fixture('stack') + const current = decodeJsconfuserVm(source) + const wrong = current.output.replace('= 6;', '= 60;') + expect(wrong).not.toBe(current.output) + expect(exactEqual(observeKnown(wrong), observeKnown(fixture('stack', '.src.js')))).toBe(false) +}) + +// The producer omits JUMP_REG from its nonterminal macro exclusion. This +// source-admitted synthetic macro passes the PATCH stage, which reads widths +// only; after macro expansion the backend's control solver refuses the register jump. +test('refuses a source-reachable nonterminal macro register jump', () => { + const ast = parse(fixture('macro')) + let jumpBody, loadBody, macro + traverse(ast, { SwitchCase(path) { + const node = path.node + if (t.isMemberExpression(node.test) && t.isIdentifier(node.test.property, { name: 'JUMP_REG' })) jumpBody = node.consequent + if (t.isMemberExpression(node.test) && t.isIdentifier(node.test.property, { name: 'LOAD_INT' })) loadBody = node.consequent + if (t.isNumericLiteral(node.test, { value: 61 })) macro = node + } }) + expect(jumpBody).toBeDefined() + expect(loadBody).toBeDefined() + const body = statements => statements.flatMap(statement => t.isBlockStatement(statement) ? statement.body : [statement]).filter(statement => !t.isBreakStatement(statement)) + macro.consequent = [t.blockStatement([...body(jumpBody), ...body(loadBody)].map(node => t.cloneNode(node, true)).concat(t.breakStatement()))] + const clear = [61, 0, 0, 7] + let key = (7 ^ 5) | 0 + const cipher = clear.map(word => { key = (key + 0x9e3779b9) | 0; return (word ^ key ^ (key >>> 13)) >>> 0 }) + const words = [56, 5, 11, 15, 7, 0, 0, 0, 0, 45, 0, ...cipher] + traverse(ast, { VariableDeclarator(path) { if (t.isIdentifier(path.node.id, { name: 'BYTECODE' })) path.node.init = t.arrayExpression(words.map(word => t.numericLiteral(word))) } }) + const source = generate(ast).code + const container = diagnoseContainer(source) + expect(container.ok).toBe(true) + const addedOpcodes = diagnosePatchAddedOpcodes(source) + expect(addedOpcodes.ok).toBe(true) + expect(addedOpcodes.result.macro.macros[0].steps.map(step => step.name)).toEqual(['JUMP_REG', 'LOAD_INT']) + expect(diagnoseSelfModifyingRegions(container.result, addedOpcodes.result).ok).toBe(true) + expect(decodeJsconfuserVm(source)).toMatchObject({ ok: false, output: source, diagnostic: { code: 'unresolved-indirect-jump' } }) +}) diff --git a/test/vm/jsconfuser-vm/qualification-provenance.test.js b/test/vm/jsconfuser-vm/qualification-provenance.test.js new file mode 100644 index 00000000..4c171eeb --- /dev/null +++ b/test/vm/jsconfuser-vm/qualification-provenance.test.js @@ -0,0 +1,55 @@ +// Record consistency only: nothing here executes a retained program. The native +// replay of each record's observations is qualification-replay.test.js. +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { + focusedOracles, + readCell, + readOracle, + records, +} from './qualification-records.js' + +test.each(records())( + 'retained cell %s has a consistent record', + (filename) => { + const { record, sourcePath, encodedPath } = readCell(filename) + expect(fs.existsSync(sourcePath)).toBe(true) + expect(fs.existsSync(encodedPath)).toBe(true) + expect(record.sourceObservation).toEqual(record.encodedObservation) + expect(record.id).toBe(`${record.case}--${record.profile}--${record.seed}`) + expect( + Number.isInteger(record.seed) && + record.seed >= 0 && + record.seed <= 0xffffffff, + ).toBe(true) + expect(record.options.target).toBe('browser') + if (record.status === 'accepted') { + expect(record.decodedObservation).toEqual(record.sourceObservation) + expect(record.diagnostic).toBeNull() + } else { + expect(record.status).toBe('declined') + expect(record.decodedObservation).toBeNull() + expect(record.diagnostic.code).toBeTypeOf('string') + } + const components = record.effectObservation.components + if (components) { + for (const option of Object.keys(components)) + expect(record.options[option]).toBe(true) + const expected = Object.values(components).includes(false) + ? false + : Object.values(components).includes(null) + ? null + : true + expect(record.effectObservation.observed).toBe(expected) + } + expect(record.provenance.generation.encoderCommit).toBe( + record.encoderCommit ?? null, + ) + expect(record.provenance.completeEffectControlsRetained).toBe(false) + expect(record.provenance.decodedBytesRetained).toBe(false) + }, +) + +test.each(focusedOracles)('focused oracle %s names its cell', (name) => { + expect(readOracle(name).oracle.cellId).toBe(name) +}) diff --git a/test/vm/jsconfuser-vm/qualification-records.js b/test/vm/jsconfuser-vm/qualification-records.js new file mode 100644 index 00000000..46accc9a --- /dev/null +++ b/test/vm/jsconfuser-vm/qualification-records.js @@ -0,0 +1,41 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +export const root = fileURLToPath(new URL('fixtures/', import.meta.url)) +export const focusedOracles = [ + 'caught-return-through-finally', + 'protected-conditional-catch', + 'return-through-finally', + 'terminal-switch-arms', +] +export function records(directory = root) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const name = path.join(directory, entry.name) + return entry.isDirectory() + ? records(name) + : /^qualification(?:-\d+)?\.json$/.test(entry.name) + ? [name] + : [] + }) +} +// A record's suffix names its encoded file; the common source file is shared. +export function readCell(filename) { + const record = JSON.parse(fs.readFileSync(filename, 'utf8')) + const suffix = path.basename(filename, '.json').slice('qualification'.length) + const directory = path.dirname(filename) + return { + record, + sourcePath: path.join(directory, 'source.js'), + encodedPath: path.join(directory, `encoded${suffix}.js`), + } +} +export function readOracle(name) { + const directory = path.join(root, 'corpus/focused', name) + return { + oracle: JSON.parse( + fs.readFileSync(path.join(directory, 'oracle.json'), 'utf8'), + ), + directory, + } +} diff --git a/test/vm/jsconfuser-vm/qualification-replay.test.js b/test/vm/jsconfuser-vm/qualification-replay.test.js new file mode 100644 index 00000000..fcb18b19 --- /dev/null +++ b/test/vm/jsconfuser-vm/qualification-replay.test.js @@ -0,0 +1,52 @@ +// Native replay of retained programs against their recorded observations. Git +// already fixes the bytes, so this detects a host change (a Node upgrade, say) +// altering what a frozen program does. Heavy: excluded from the light run. +import fs from 'node:fs' +import path from 'node:path' +import vm from 'node:vm' +import { expect, test } from 'vitest' +import { + focusedOracles, + readCell, + readOracle, + records, +} from './qualification-records.js' + +function observe(source) { + const context = { + TEST_OUTPUT: null, + performance: { now: () => 0 }, + atob, + btoa, + } + context.window = context + vm.runInNewContext(source, context, { timeout: 30000 }) + return { + kind: 'value', + value: JSON.parse(JSON.stringify(context.TEST_OUTPUT)), + } +} + +test.each(records())( + 'retained cell %s still produces its recorded observations', + (filename) => { + const { record, sourcePath, encodedPath } = readCell(filename) + expect(observe(fs.readFileSync(sourcePath, 'utf8'))).toEqual( + record.sourceObservation, + ) + expect(observe(fs.readFileSync(encodedPath, 'utf8'))).toEqual( + record.encodedObservation, + ) + }, +) + +test.each(focusedOracles)( + 'focused oracle %s matches its source/encoded behavior', + (name) => { + const { oracle, directory } = readOracle(name) + for (const kind of ['source', 'encoded']) { + const source = fs.readFileSync(path.join(directory, `${kind}.js`), 'utf8') + expect(observe(source)).toEqual({ kind: 'value', value: oracle.expected }) + } + }, +) diff --git a/test/vm/jsconfuser-vm/runtime-finalizers.behavior.test.js b/test/vm/jsconfuser-vm/runtime-finalizers.behavior.test.js new file mode 100644 index 00000000..cae0642a --- /dev/null +++ b/test/vm/jsconfuser-vm/runtime-finalizers.behavior.test.js @@ -0,0 +1,186 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { inspectCurrent } from '../../../scripts/qualify-jsconfuser-vm/oracles.mjs' +import { + timingProfiles, + timingSources, + assertTimingBehavior, +} from '../../../scripts/qualify-jsconfuser-vm/timing.mjs' +import { + timingSourceRoles, + observeTimingKnown, + runTimingNative, +} from '../../../scripts/qualify-jsconfuser-vm/timing-observation.mjs' +import { diagnoseNormalizeSelfModifyingWordcode } from '../../../src/vm/jsconfuser-vm/normalize-self-modifying-wordcode.js' +import { diagnoseWordcode } from '../../../src/vm/jsconfuser-vm/read-wordcode.js' + +const fixture = (name) => + fs.readFileSync( + new URL(`fixtures/runtime-finalizers/${name}`, import.meta.url), + 'utf8', + ) +const original = fixture('finalizers.src.js') +const oracle = JSON.parse(fixture('oracle.json')) + +test('retained generation metadata separates population inputs from the worker-only boundary', () => { + const generation = JSON.parse(fixture('generation.json')) + expect(generation.generator).toBe('scripts/qualify-jsconfuser-vm/timing.mjs') + expect(generation.cases).toEqual({ finalizers: 'finalizers.src.js' }) + expect(original.trim()).toBe(timingSources.finalizers) + expect(generation.profiles).toEqual( + Object.fromEntries( + timingProfiles.map(({ name, options }) => [name, options]), + ), + ) + const boundary = generation.separateGeneration.topLevelFinally + expect(boundary).toMatchObject({ + worker: 'scripts/qualify-jsconfuser-vm/worker.mjs', + driver: 'scripts/qualify-jsconfuser-vm/qualify.mjs', + api: 'runWorker(request, 65000)', + sourceFile: 'top-level-finally.src.js', + outputFile: 'top-level-finally.js', + request: { + action: 'generate', + seed: 231, + options: generation.baselineOptions, + }, + rng: generation.rng, + toolchain: generation.toolchain, + encoderPin: generation.encoderPin, + }) + expect(fixture(boundary.sourceFile)).toContain('finally') + expect(fixture(boundary.outputFile)).toContain('TIMING_CHECKS = false') +}) + +for (const profile of timingProfiles) + test(`current ${profile.name} finalizer recovery, real clocks and consumed state`, async () => { + const source = fixture(`${profile.name}.js`), + roles = timingSourceRoles(source) + const current = inspectCurrent(source) + const active = Boolean(profile.options.timingChecks) + expect(current.publicRecord.ok).toBe(true) + expect(current.publicRecord.output).not.toBe(source) + expect(current.checks.ok).toBe(true) + expect(current.inputState).toBe(true) + expect(current.publicRecord.result.finalJavaScriptSemantics).toBe(false) + const equal = observeTimingKnown(source, { + vmName: roles.vmName, + clock: (index) => index * 100, + }) + assertTimingBehavior(equal.behavior, { + kind: 'value', + value: oracle.normal, + }) + for (const mode of ['fast', 'slow', 'paused']) { + const sourceRun = await runTimingNative({ source: original, mode }) + const encodedRun = await runTimingNative({ + source, + mode, + vmName: roles.vmName, + }) + assertTimingBehavior(sourceRun.result.behavior, { + kind: 'value', + value: oracle.normal, + }) + expect(encodedRun.process).toMatchObject({ + exitCode: 0, + signal: null, + timedOut: false, + }) + if (active && mode !== 'fast') { + expect(encodedRun.result.behavior.kind).toBe('error') + expect(oracle.poisonErrorKinds).toContain( + encodedRun.result.behavior.name, + ) + expect(encodedRun.result.state).toMatchObject({ + wordsZero: true, + frame: undefined, + cleaned: undefined, + currentFramePayloadCleared: true, + }) + expect( + encodedRun.result.state.captures.some( + (capture) => !capture.closed && capture.backing === undefined, + ), + ).toBe(true) + expect( + encodedRun.result.state.captures.some( + (capture) => capture.backing?.runtimeType === 'function', + ), + ).toBe(true) + expect(encodedRun.result.blockedMs).toBeGreaterThan( + oracle.activeThreshold, + ) + } else + assertTimingBehavior( + encodedRun.result.behavior, + sourceRun.result.behavior, + ) + // The decoded program carries no timing check: it behaves as the source in every mode. + const decodedRun = await runTimingNative({ + source: current.publicRecord.output, + mode, + }) + assertTimingBehavior( + decodedRun.result.behavior, + sourceRun.result.behavior, + ) + if (!active) + expect(encodedRun.result.state).toMatchObject({ + frame: 0, + top: 1, + captures: [], + cleaned: true, + }) + if (mode === 'paused') + expect(decodedRun.process.pause?.resumeSignal).toBe('SIGCONT') + if (mode === 'paused') { + expect(sourceRun.process.pause?.resumeSignal).toBe('SIGCONT') + expect(encodedRun.process.pause?.resumeSignal).toBe('SIGCONT') + } + } + }) + +// The PATCH stage removes only its own words; added opcodes stay for their own routes, so +// canonical readWordcode output exists only after them. The consumed model holds neither. +test('retained strongest PATCH peer leaves its added opcodes to their own routes', () => { + const source = fixture('patch.js'), + current = inspectCurrent(source) + const container = current.container.result, + before = JSON.stringify(container) + const normalized = diagnoseNormalizeSelfModifyingWordcode(container, { + source, + }) + expect(normalized.ok).toBe(true) + const result = normalized.result + const B = diagnoseWordcode(result.container) + expect(result.wordcode).toEqual(B.ok ? B.result : null) + expect(result.wordCount).toBeLessThan(result.originalWordCount) + expect(JSON.stringify(container)).toBe(before) + expect(current.analysis.ok).toBe(true) + expect( + current.analysis.model.wordcode.instructions.some(({ name }) => + ['PATCH', 'ALIAS', 'MACRO', 'SPECIALIZED', 'SYNTHETIC'].includes(name), + ), + ).toBe(false) +}) + +test('top-level normal-continuation finally recovers with source behaviour', async () => { + const source = fixture('top-level-finally.js'), + current = inspectCurrent(source) + expect(current.container.ok).toBe(true) + expect(current.analysis.ok).toBe(true) + expect(current.publicRecord.ok).toBe(true) + expect(current.publicRecord.output).not.toBe(source) + expect(current.checks.ok).toBe(true) + expect(current.inputState).toBe(true) + const topLevelSource = fixture('top-level-finally.src.js') + for (const mode of ['fast', 'slow', 'paused']) { + const sourceRun = await runTimingNative({ source: topLevelSource, mode }) + const decodedRun = await runTimingNative({ + source: current.publicRecord.output, + mode, + }) + assertTimingBehavior(decodedRun.result.behavior, sourceRun.result.behavior) + } +}) diff --git a/test/vm/jsconfuser-vm/runtime-finalizers.test.js b/test/vm/jsconfuser-vm/runtime-finalizers.test.js new file mode 100644 index 00000000..37800a56 --- /dev/null +++ b/test/vm/jsconfuser-vm/runtime-finalizers.test.js @@ -0,0 +1,105 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' +import { decodeJsconfuserVm } from '../../../src/plugin/jsconfuser-vm.js' +import { inspectCurrent } from '../../../scripts/qualify-jsconfuser-vm/oracles.mjs' +import { + timingSourceRoles, + observeTimingKnown, +} from '../../../scripts/qualify-jsconfuser-vm/timing-observation.mjs' + +const reference = fs.readFileSync( + new URL('fixtures/reference/encoded.js', import.meta.url), + 'utf8', +) +const finalizers = fs.readFileSync( + new URL('fixtures/runtime-finalizers/baseline.js', import.meta.url), + 'utf8', +) +const timing = (source, value) => + source.replace('var TIMING_CHECKS = false;', `var TIMING_CHECKS = ${value};`) + +// Timing checks live in the interpreter only: every TIMING_CHECKS value decodes to the program the +// disabled runtime decodes to, so the decoded program carries no timing check. +test('every timing value decodes to the disabled program', () => { + const disabled = decodeJsconfuserVm(reference) + expect(disabled.ok).toBe(true) + for (const value of ['false', '0', '-0', '100', '1000', '-1']) { + const source = timing(reference, value) + const result = inspectCurrent(source) + expect(result.publicRecord.ok).toBe(true) + expect(result.checks.ok).toBe(true) + expect(result.inputState).toBe(true) + expect(result.publicRecord.output).toBe(disabled.output) + } +}) + +test('a renamed Upvalue close method decodes and behaves as the source', () => { + const renamed = finalizers + .replace('Upvalue.prototype._close =', 'Upvalue.prototype._seal =') + .replaceAll('uv._close()', 'uv._seal()') + const current = inspectCurrent(renamed) + expect(current.container.ok).toBe(true) + expect(current.publicRecord.ok).toBe(true) + expect(current.checks.ok).toBe(true) + expect(current.inputState).toBe(true) + expect(observeTimingKnown(current.publicRecord.output).behavior).toEqual({ + kind: 'value', + value: [7, true, 7], + }) +}) + +test('equal threshold preserves behavior and greater threshold poisons before dispatch', () => { + const source = timing(reference, '100'), + roles = timingSourceRoles(source) + const equal = observeTimingKnown(source, { + vmName: roles.vmName, + clock: (index) => index * 100, + }) + expect(equal.behavior).toEqual( + observeTimingKnown(reference, { clock: () => 0 }).behavior, + ) + const greater = observeTimingKnown(source, { + vmName: roles.vmName, + clock: (index) => (index === 0 ? 0 : 101), + }) + expect(greater.behavior).toMatchObject({ + kind: 'error', + name: 'Error', + message: 'Unknown opcode: undefined at pc NaN', + }) + expect(greater.state).toMatchObject({ + wordsZero: true, + frame: undefined, + currentFramePayloadCleared: true, + }) +}) + +test('clock side effects are an explicit native host boundary', () => { + const decoded = decodeJsconfuserVm(reference) + expect(decoded.ok).toBe(true) + const clock = () => { + throw new Error('host clock failed') + } + expect(observeTimingKnown(reference, { clock }).behavior).toMatchObject({ + kind: 'error', + message: 'host clock failed', + }) + expect(observeTimingKnown(decoded.output, { clock }).behavior.kind).toBe( + 'value', + ) + expect(decoded.result.finalJavaScriptSemantics).toBe(false) +}) + +test('active timing decodes with numeric transport and with a handler table', () => { + for (const fixture of ['encoded-bytecode-arithmetic', 'handler-table-boot']) { + const disabled = fs.readFileSync( + new URL(`fixtures/${fixture}/encoded.js`, import.meta.url), + 'utf8', + ) + const source = timing(disabled, '100') + expect(source).toContain('var TIMING_CHECKS = 100;') + const decoded = decodeJsconfuserVm(source) + expect(decoded.ok).toBe(true) + expect(decoded.output).toBe(decodeJsconfuserVm(disabled).output) + } +}) diff --git a/vitest.config.js b/vitest.config.js index 52cdc5d2..fccf7b06 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,5 +1,5 @@ import path from 'path' -import { defineConfig } from 'vitest/config' +import { configDefaults, defineConfig } from 'vitest/config' export default defineConfig({ test: { @@ -15,6 +15,9 @@ export default defineConfig({ "reporter": ['text', 'json', 'json-summary'], "reportOnFailure": true, }, + // sandbox-tests/ holds encoder checkouts fetched for qualification + // (scripts/qualify-jsconfuser-vm); their own suites are not decoder tests. + "exclude": [...configDefaults.exclude, 'sandbox-tests/**'], "passWithNoTests": true, // The heaviest jsconfuser and VM cases decode whole real-encoder samples, // and their time grows several-fold under v8 coverage and full parallel